@pugi/cli 0.1.0-beta.99 → 1.0.0-alpha.10
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/LICENSE +1 -1
- package/README.md +11 -191
- package/bin/pugi +8 -0
- package/package.json +15 -71
- package/postinstall.mjs +31 -0
- package/CHANGELOG.md +0 -132
- package/THIRD_PARTY_NOTICES.md +0 -40
- package/assets/pugi-mascot.ansi +0 -16
- package/assets/pugi-prozr2-mascot.ansi +0 -9
- package/bin/run.js +0 -34
- package/dist/commands/deploy.js +0 -439
- package/dist/commands/flatten.js +0 -191
- package/dist/commands/jobs-watch.js +0 -201
- package/dist/commands/jobs.js +0 -260
- package/dist/commands/retro.js +0 -210
- package/dist/commands/smoke.js +0 -133
- package/dist/core/agent-progress/cleanup.js +0 -134
- package/dist/core/agent-progress/schema.js +0 -144
- package/dist/core/agent-progress/writer.js +0 -101
- package/dist/core/agents/adaptive-router.js +0 -330
- package/dist/core/agents/loader.js +0 -104
- package/dist/core/agents/query-decomposer.js +0 -297
- package/dist/core/agents/registry.js +0 -69
- package/dist/core/approvals/shortcut-resolver.js +0 -98
- package/dist/core/artifact-chain/dispatcher.js +0 -148
- package/dist/core/artifact-chain/exporter.js +0 -164
- package/dist/core/artifact-chain/state.js +0 -243
- package/dist/core/artifact-chain/steps.js +0 -169
- package/dist/core/ask-user/question.js +0 -92
- package/dist/core/audit/audit-trail.js +0 -275
- package/dist/core/auth/ensure-authenticated.js +0 -129
- package/dist/core/auth/env-provider.js +0 -238
- package/dist/core/auto-open-browser.js +0 -128
- package/dist/core/auto-update/channels.js +0 -122
- package/dist/core/auto-update/checker.js +0 -241
- package/dist/core/auto-update/state.js +0 -235
- package/dist/core/bare-mode/index.js +0 -107
- package/dist/core/bash/redirect.js +0 -281
- package/dist/core/bash-classifier.js +0 -1397
- package/dist/core/checkpoint/resumer.js +0 -149
- package/dist/core/checkpoint/rewinder.js +0 -291
- package/dist/core/checkpoints/shadow-git.js +0 -670
- package/dist/core/citations/parser.js +0 -109
- package/dist/core/classifier/yolo-classifier.js +0 -88
- package/dist/core/clipboard.js +0 -70
- package/dist/core/codegraph/decision-store.js +0 -248
- package/dist/core/codegraph/detect-repo.js +0 -459
- package/dist/core/codegraph/install.js +0 -134
- package/dist/core/codegraph/offer-hook.js +0 -220
- package/dist/core/compact/auto-trigger.js +0 -96
- package/dist/core/compact/buffer-rewriter.js +0 -115
- package/dist/core/compact/summarizer.js +0 -208
- package/dist/core/compact/token-counter.js +0 -108
- package/dist/core/consensus/anvil-fanout.js +0 -276
- package/dist/core/consensus/diff-capture.js +0 -491
- package/dist/core/consensus/rubric.js +0 -233
- package/dist/core/context/builder.js +0 -114
- package/dist/core/context/compaction-events.js +0 -99
- package/dist/core/context/compaction.js +0 -602
- package/dist/core/context/index.js +0 -28
- package/dist/core/context/invariants.js +0 -250
- package/dist/core/context/markdown-loader.js +0 -288
- package/dist/core/context/markdown-traverse.js +0 -255
- package/dist/core/context/pugiignore.js +0 -316
- package/dist/core/context/repo-skeleton.js +0 -533
- package/dist/core/context/tool-eviction.js +0 -55
- package/dist/core/context/watcher.js +0 -342
- package/dist/core/context/working-set.js +0 -165
- package/dist/core/coordinator/agent-tools.js +0 -77
- package/dist/core/coordinator/agent-toolset.js +0 -65
- package/dist/core/coordinator/fsm.js +0 -73
- package/dist/core/coordinator/mode-fsm.js +0 -70
- package/dist/core/cost/rate-card.js +0 -129
- package/dist/core/cost/tracker.js +0 -221
- package/dist/core/credentials.js +0 -355
- package/dist/core/cron/scheduler.js +0 -138
- package/dist/core/denial-tracking/index.js +0 -8
- package/dist/core/denial-tracking/state.js +0 -264
- package/dist/core/diagnostics/probe-runner.js +0 -93
- package/dist/core/diagnostics/probes/api.js +0 -46
- package/dist/core/diagnostics/probes/auth.js +0 -93
- package/dist/core/diagnostics/probes/bare-mode.js +0 -42
- package/dist/core/diagnostics/probes/cli-version.js +0 -127
- package/dist/core/diagnostics/probes/config.js +0 -72
- package/dist/core/diagnostics/probes/denial-tracking.js +0 -57
- package/dist/core/diagnostics/probes/disk.js +0 -81
- package/dist/core/diagnostics/probes/engine-live.js +0 -46
- package/dist/core/diagnostics/probes/git.js +0 -65
- package/dist/core/diagnostics/probes/hooks.js +0 -118
- package/dist/core/diagnostics/probes/mcp.js +0 -75
- package/dist/core/diagnostics/probes/node.js +0 -59
- package/dist/core/diagnostics/probes/pnpm.js +0 -36
- package/dist/core/diagnostics/probes/pugi-md.js +0 -89
- package/dist/core/diagnostics/probes/sandbox.js +0 -72
- package/dist/core/diagnostics/probes/session.js +0 -74
- package/dist/core/diagnostics/probes/status-snapshot.js +0 -488
- package/dist/core/diagnostics/probes/workspace.js +0 -63
- package/dist/core/diagnostics/types.js +0 -70
- package/dist/core/dispatch/cache-cleanup.js +0 -197
- package/dist/core/dispatch/cache-handoff.js +0 -295
- package/dist/core/edits/apply-patch-layer-e.js +0 -189
- package/dist/core/edits/dispatch.js +0 -511
- package/dist/core/edits/format-detector.js +0 -260
- package/dist/core/edits/format-matrix.js +0 -26
- package/dist/core/edits/fuzzy-ladder.js +0 -650
- package/dist/core/edits/index.js +0 -19
- package/dist/core/edits/journal.js +0 -199
- package/dist/core/edits/layer-a-apply.js +0 -217
- package/dist/core/edits/layer-a-fuzzy-apply.js +0 -198
- package/dist/core/edits/layer-b-apply.js +0 -211
- package/dist/core/edits/layer-c-apply.js +0 -160
- package/dist/core/edits/layer-d-ast.js +0 -572
- package/dist/core/edits/marker-parser.js +0 -401
- package/dist/core/edits/security-gate.js +0 -223
- package/dist/core/edits/verify-hook.js +0 -273
- package/dist/core/edits/worktree.js +0 -322
- package/dist/core/engine/adapter-runner.js +0 -8
- package/dist/core/engine/anvil-client.js +0 -344
- package/dist/core/engine/auto-compact.js +0 -179
- package/dist/core/engine/budgets.js +0 -195
- package/dist/core/engine/context-prefix.js +0 -155
- package/dist/core/engine/index.js +0 -12
- package/dist/core/engine/intensity.js +0 -163
- package/dist/core/engine/intent.js +0 -260
- package/dist/core/engine/native-pugi.js +0 -1616
- package/dist/core/engine/noop.js +0 -27
- package/dist/core/engine/prompts.js +0 -236
- package/dist/core/engine/strip-internal-fields.js +0 -124
- package/dist/core/engine/tool-bridge.js +0 -2173
- package/dist/core/engine/verification-patterns.js +0 -195
- package/dist/core/evaluation/golden-dataset.js +0 -293
- package/dist/core/feedback/queue.js +0 -177
- package/dist/core/feedback/submitter.js +0 -145
- package/dist/core/file-cache.js +0 -141
- package/dist/core/flatten/flatten-repo.js +0 -439
- package/dist/core/format/osc8-link.js +0 -28
- package/dist/core/hook-chains.js +0 -392
- package/dist/core/hooks/citation-verify-hook.js +0 -138
- package/dist/core/hooks/citation-verify.js +0 -112
- package/dist/core/hooks/events.js +0 -46
- package/dist/core/hooks/index.js +0 -15
- package/dist/core/hooks/registry.js +0 -216
- package/dist/core/hooks/runner.js +0 -236
- package/dist/core/hooks/v2/event-emitter.js +0 -115
- package/dist/core/hooks/v2/executor.js +0 -282
- package/dist/core/hooks/v2/index.js +0 -25
- package/dist/core/hooks/v2/lifecycle.js +0 -104
- package/dist/core/hooks/v2/loader.js +0 -216
- package/dist/core/hooks/v2/matcher.js +0 -125
- package/dist/core/hooks/v2/trust.js +0 -143
- package/dist/core/hooks/v2/types.js +0 -86
- package/dist/core/hooks/worktree-events.js +0 -158
- package/dist/core/hooks.js +0 -415
- package/dist/core/image/renderer.js +0 -71
- package/dist/core/index-store.js +0 -260
- package/dist/core/init/detector.js +0 -582
- package/dist/core/init/template-renderer.js +0 -242
- package/dist/core/jobs/registry.js +0 -462
- package/dist/core/ledger/results-tsv.js +0 -142
- package/dist/core/log-discipline/stdout-redirect.js +0 -51
- package/dist/core/lsp/cache.js +0 -105
- package/dist/core/lsp/client.js +0 -1229
- package/dist/core/lsp/language-detect.js +0 -66
- package/dist/core/lsp/post-edit-diagnostics.js +0 -171
- package/dist/core/lsp/server-detect.js +0 -173
- package/dist/core/lsp/symbol-cache.js +0 -162
- package/dist/core/lsp/symbol-tools.js +0 -664
- package/dist/core/mcp/client.js +0 -385
- package/dist/core/mcp/http-server.js +0 -553
- package/dist/core/mcp/orchestrator-config.js +0 -192
- package/dist/core/mcp/orchestrator-tools.js +0 -806
- package/dist/core/mcp/permission.js +0 -190
- package/dist/core/mcp/registry.js +0 -193
- package/dist/core/mcp/server-tools.js +0 -219
- package/dist/core/mcp/server.js +0 -397
- package/dist/core/mcp/trust.js +0 -91
- package/dist/core/memory/dual-write.js +0 -416
- package/dist/core/memory/passive-extract.js +0 -130
- package/dist/core/memory/phase1-kinds.js +0 -20
- package/dist/core/memory/secret-scanner.js +0 -304
- package/dist/core/memory-sync/queue.js +0 -170
- package/dist/core/metrics/extract.js +0 -113
- package/dist/core/modes/roo-modes.js +0 -68
- package/dist/core/onboarding/ensure-initialized.js +0 -133
- package/dist/core/onboarding/marker.js +0 -111
- package/dist/core/onboarding/telemetry-state.js +0 -108
- package/dist/core/output-style/presets.js +0 -176
- package/dist/core/output-style/state.js +0 -185
- package/dist/core/path-security.js +0 -345
- package/dist/core/permission.js +0 -369
- package/dist/core/permissions/auto-classifier.js +0 -124
- package/dist/core/permissions/bash-parser.js +0 -371
- package/dist/core/permissions/circuit-breaker.js +0 -83
- package/dist/core/permissions/constrained-edit.js +0 -91
- package/dist/core/permissions/gate.js +0 -278
- package/dist/core/permissions/index.js +0 -20
- package/dist/core/permissions/mode.js +0 -174
- package/dist/core/permissions/network-egress.js +0 -137
- package/dist/core/permissions/state.js +0 -241
- package/dist/core/permissions/tool-class.js +0 -107
- package/dist/core/plan-mode/ui-state.js +0 -51
- package/dist/core/plans/plan-artifact.js +0 -721
- package/dist/core/policy-limits/etag-store.js +0 -122
- package/dist/core/prd-check/parser.js +0 -215
- package/dist/core/prd-check/reporter.js +0 -127
- package/dist/core/prd-check/session-review.js +0 -557
- package/dist/core/prd-check/verifiers.js +0 -223
- package/dist/core/prompt-cache/client-cache.js +0 -99
- package/dist/core/prompts/assembly.js +0 -29
- package/dist/core/prompts/registry.js +0 -364
- package/dist/core/pugi-gitignore.js +0 -52
- package/dist/core/pugi-md/cc-compat-rules.js +0 -735
- package/dist/core/pugi-md/context-injector.js +0 -76
- package/dist/core/pugi-md/walk-up.js +0 -207
- package/dist/core/python/uv-installer.js +0 -270
- package/dist/core/python/uv-resolver.js +0 -83
- package/dist/core/rate-limit/narrator.js +0 -146
- package/dist/core/recipes/cli-types.js +0 -20
- package/dist/core/recipes/loader.js +0 -103
- package/dist/core/recipes/runner.js +0 -345
- package/dist/core/recipes/schema.js +0 -587
- package/dist/core/release-notes/parser.js +0 -241
- package/dist/core/release-notes/state.js +0 -116
- package/dist/core/repl/ask.js +0 -512
- package/dist/core/repl/cancellation.js +0 -98
- package/dist/core/repl/cap-warning.js +0 -91
- package/dist/core/repl/clipboard-read.js +0 -174
- package/dist/core/repl/dispatch-fsm.js +0 -220
- package/dist/core/repl/engine-bridge.js +0 -303
- package/dist/core/repl/history-search.js +0 -175
- package/dist/core/repl/history.js +0 -182
- package/dist/core/repl/kill-ring.js +0 -138
- package/dist/core/repl/model-pricing.js +0 -135
- package/dist/core/repl/privacy-banner.js +0 -71
- package/dist/core/repl/session.js +0 -4962
- package/dist/core/repl/slash-commands.js +0 -747
- package/dist/core/repl/store/index.js +0 -12
- package/dist/core/repl/store/jsonl-log.js +0 -321
- package/dist/core/repl/store/lockfile.js +0 -155
- package/dist/core/repl/store/session-store.js +0 -821
- package/dist/core/repl/store/types.js +0 -44
- package/dist/core/repl/store/uuid-v7.js +0 -68
- package/dist/core/repl/tool-route.js +0 -382
- package/dist/core/repl/workspace-context.js +0 -206
- package/dist/core/repo-map/build.js +0 -125
- package/dist/core/repo-map/cache.js +0 -185
- package/dist/core/repo-map/extractor.js +0 -254
- package/dist/core/repo-map/formatter.js +0 -145
- package/dist/core/repo-map/page-rank.js +0 -105
- package/dist/core/repo-map/scanner.js +0 -211
- package/dist/core/retro/git-collector.js +0 -251
- package/dist/core/retro/health-card.js +0 -25
- package/dist/core/retro/metrics.js +0 -342
- package/dist/core/retro/narrative.js +0 -249
- package/dist/core/retro/plane-collector.js +0 -274
- package/dist/core/retro/pr-issue-link.js +0 -65
- package/dist/core/retro/types.js +0 -16
- package/dist/core/retry-budget/budget.js +0 -284
- package/dist/core/retry-budget/index.js +0 -5
- package/dist/core/retry-budget/retry-cap.js +0 -74
- package/dist/core/routing/lead-worker.js +0 -43
- package/dist/core/routing/pre-flight-estimator.js +0 -108
- package/dist/core/runs/run-tree.js +0 -103
- package/dist/core/sandboxing/adapter.js +0 -29
- package/dist/core/sandboxing/index.js +0 -49
- package/dist/core/sandboxing/none.js +0 -19
- package/dist/core/sandboxing/seatbelt.js +0 -183
- package/dist/core/security/injection-scanner.js +0 -367
- package/dist/core/security/output-filter.js +0 -418
- package/dist/core/session/env-file.js +0 -105
- package/dist/core/session/section-budgets.js +0 -140
- package/dist/core/session.js +0 -377
- package/dist/core/settings.js +0 -400
- package/dist/core/share/formatter.js +0 -271
- package/dist/core/share/redactor.js +0 -221
- package/dist/core/share/uploader.js +0 -267
- package/dist/core/skills/defaults.js +0 -457
- package/dist/core/skills/loader.js +0 -454
- package/dist/core/skills/sources.js +0 -480
- package/dist/core/skills/trust.js +0 -172
- package/dist/core/smoke/headless-driver.js +0 -174
- package/dist/core/smoke/orchestrator.js +0 -194
- package/dist/core/smoke/runner.js +0 -238
- package/dist/core/smoke/scenario-parser.js +0 -316
- package/dist/core/statusline.js +0 -99
- package/dist/core/subagents/dispatcher-real.js +0 -600
- package/dist/core/subagents/dispatcher.js +0 -352
- package/dist/core/subagents/index.js +0 -39
- package/dist/core/subagents/isolation-matrix.js +0 -213
- package/dist/core/subagents/spawn.js +0 -101
- package/dist/core/telemetry/emitter.js +0 -229
- package/dist/core/telemetry/queue.js +0 -251
- package/dist/core/theme/context.js +0 -91
- package/dist/core/theme/presets.js +0 -228
- package/dist/core/theme/state.js +0 -181
- package/dist/core/todos/invariant.js +0 -10
- package/dist/core/todos/state.js +0 -177
- package/dist/core/tool-schema/compressor.js +0 -89
- package/dist/core/transport/version-interceptor.js +0 -166
- package/dist/core/trust.js +0 -109
- package/dist/core/tui/thinking-block.js +0 -64
- package/dist/core/vim/keymap.js +0 -288
- package/dist/core/vim/state.js +0 -92
- package/dist/core/watch-markers/marker-watcher.js +0 -133
- package/dist/core/worktree/include-parser.js +0 -249
- package/dist/core/worktree-manager/cleanup.js +0 -123
- package/dist/core/worktree-manager/manager.js +0 -303
- package/dist/index.js +0 -44
- package/dist/runtime/bootstrap.js +0 -190
- package/dist/runtime/cli.js +0 -8121
- package/dist/runtime/commands/agents.js +0 -385
- package/dist/runtime/commands/budget.js +0 -192
- package/dist/runtime/commands/cancel.js +0 -231
- package/dist/runtime/commands/chain.js +0 -489
- package/dist/runtime/commands/codegraph-status.js +0 -227
- package/dist/runtime/commands/compact.js +0 -297
- package/dist/runtime/commands/config.js +0 -595
- package/dist/runtime/commands/cost.js +0 -199
- package/dist/runtime/commands/delegate.js +0 -312
- package/dist/runtime/commands/dispatch.js +0 -126
- package/dist/runtime/commands/doctor.js +0 -579
- package/dist/runtime/commands/feedback.js +0 -184
- package/dist/runtime/commands/hooks.js +0 -187
- package/dist/runtime/commands/init.js +0 -254
- package/dist/runtime/commands/lsp.js +0 -368
- package/dist/runtime/commands/mcp.js +0 -935
- package/dist/runtime/commands/memory.js +0 -582
- package/dist/runtime/commands/model.js +0 -237
- package/dist/runtime/commands/onboarding.js +0 -275
- package/dist/runtime/commands/patch.js +0 -128
- package/dist/runtime/commands/permissions.js +0 -112
- package/dist/runtime/commands/plan.js +0 -143
- package/dist/runtime/commands/prd-check.js +0 -285
- package/dist/runtime/commands/privacy.js +0 -107
- package/dist/runtime/commands/recipe.js +0 -325
- package/dist/runtime/commands/redo-blob-store.js +0 -92
- package/dist/runtime/commands/redo.js +0 -361
- package/dist/runtime/commands/release-notes.js +0 -229
- package/dist/runtime/commands/repo-map.js +0 -95
- package/dist/runtime/commands/report.js +0 -299
- package/dist/runtime/commands/resume.js +0 -118
- package/dist/runtime/commands/review-consensus.js +0 -414
- package/dist/runtime/commands/rewind.js +0 -333
- package/dist/runtime/commands/roster.js +0 -117
- package/dist/runtime/commands/sessions.js +0 -163
- package/dist/runtime/commands/share.js +0 -316
- package/dist/runtime/commands/skills.js +0 -401
- package/dist/runtime/commands/status.js +0 -186
- package/dist/runtime/commands/stickers.js +0 -82
- package/dist/runtime/commands/style.js +0 -194
- package/dist/runtime/commands/theme.js +0 -196
- package/dist/runtime/commands/undo.js +0 -361
- package/dist/runtime/commands/update.js +0 -289
- package/dist/runtime/commands/vim.js +0 -140
- package/dist/runtime/commands/worktree.js +0 -177
- package/dist/runtime/commands/worktrees.js +0 -155
- package/dist/runtime/deprecation-warning.js +0 -69
- package/dist/runtime/engine-exit-code.js +0 -50
- package/dist/runtime/headless-repl.js +0 -195
- package/dist/runtime/headless.js +0 -548
- package/dist/runtime/load-hooks-or-exit.js +0 -71
- package/dist/runtime/plan-decompose.js +0 -531
- package/dist/runtime/sigint-guard.js +0 -272
- package/dist/runtime/stream-renderer.js +0 -195
- package/dist/runtime/update-check.js +0 -294
- package/dist/runtime/version.js +0 -65
- package/dist/runtime/worktree-bootstrap.js +0 -579
- package/dist/skills/bundled/batch.js +0 -617
- package/dist/skills/bundled/index.js +0 -45
- package/dist/skills/bundled/loop.js +0 -358
- package/dist/skills/bundled/remember.js +0 -383
- package/dist/skills/bundled/simplify.js +0 -289
- package/dist/skills/bundled/skillify.js +0 -373
- package/dist/skills/bundled/stuck.js +0 -558
- package/dist/skills/bundled/verify.js +0 -439
- package/dist/testing/vcr.js +0 -486
- package/dist/tools/agent-tool.js +0 -229
- package/dist/tools/apply-patch.js +0 -556
- package/dist/tools/ask-user-question.js +0 -337
- package/dist/tools/ask-user.js +0 -115
- package/dist/tools/bash.js +0 -1238
- package/dist/tools/brief.js +0 -224
- package/dist/tools/cron.js +0 -433
- package/dist/tools/enter-worktree.js +0 -250
- package/dist/tools/exit-worktree.js +0 -147
- package/dist/tools/file-tools.js +0 -553
- package/dist/tools/http-request.js +0 -336
- package/dist/tools/lsp-tools.js +0 -565
- package/dist/tools/mcp-tool.js +0 -260
- package/dist/tools/multi-edit.js +0 -361
- package/dist/tools/powershell.js +0 -268
- package/dist/tools/registry.js +0 -166
- package/dist/tools/server-tools.js +0 -892
- package/dist/tools/skill-tool.js +0 -96
- package/dist/tools/sleep.js +0 -99
- package/dist/tools/synthetic-output.js +0 -133
- package/dist/tools/tasks.js +0 -208
- package/dist/tools/todo-write.js +0 -184
- package/dist/tools/verify-plan-execution.js +0 -295
- package/dist/tools/web-fetch-injection-scanner.js +0 -207
- package/dist/tools/web-fetch.js +0 -720
- package/dist/tools/web-search.js +0 -458
- package/dist/tui/agent-progress-card.js +0 -111
- package/dist/tui/agent-tree-pane.js +0 -9
- package/dist/tui/agent-tree.js +0 -87
- package/dist/tui/ask-cli.js +0 -52
- package/dist/tui/ask-modal.js +0 -211
- package/dist/tui/ask-user-question-chips.js +0 -315
- package/dist/tui/ask-user-question-prompt.js +0 -203
- package/dist/tui/compact-banner.js +0 -81
- package/dist/tui/conversation-pane.js +0 -164
- package/dist/tui/cost-table.js +0 -111
- package/dist/tui/device-flow.js +0 -142
- package/dist/tui/doctor-table.js +0 -46
- package/dist/tui/feedback-prompt.js +0 -156
- package/dist/tui/input-box.js +0 -732
- package/dist/tui/login-picker.js +0 -69
- package/dist/tui/markdown-render.js +0 -266
- package/dist/tui/multi-file-diff-approval.js +0 -375
- package/dist/tui/onboarding-wizard.js +0 -240
- package/dist/tui/permissions-picker.js +0 -86
- package/dist/tui/render.js +0 -160
- package/dist/tui/repl-render.js +0 -770
- package/dist/tui/repl-splash-art.js +0 -64
- package/dist/tui/repl-splash-mascot.js +0 -154
- package/dist/tui/repl-splash.js +0 -117
- package/dist/tui/repl.js +0 -378
- package/dist/tui/slash-palette.js +0 -106
- package/dist/tui/splash-data.js +0 -61
- package/dist/tui/splash.js +0 -31
- package/dist/tui/status-bar.js +0 -209
- package/dist/tui/status-table.js +0 -7
- package/dist/tui/stickers-art.js +0 -136
- package/dist/tui/style-table.js +0 -28
- package/dist/tui/theme-table.js +0 -29
- package/dist/tui/thinking-spinner.js +0 -123
- package/dist/tui/tool-stream-pane.js +0 -140
- package/dist/tui/update-banner.js +0 -33
- package/dist/tui/vim-input.js +0 -267
- package/dist/tui/welcome-banner.js +0 -107
- package/dist/tui/welcome-data.js +0 -293
- package/dist/tui/workspace-context.js +0 -105
- package/docs/examples/codegraph.mcp.json +0 -10
- package/test/scenarios/codegen-create-file.scenario.txt +0 -13
- package/test/scenarios/compact-force.scenario.txt +0 -12
- package/test/scenarios/identity.scenario.txt +0 -11
- package/test/scenarios/persona-handoff.scenario.txt +0 -12
- package/test/scenarios/walkback.scenario.txt +0 -12
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* — Auto-update channel + last-check persistence.
|
|
3
|
-
*
|
|
4
|
-
* Two pieces of disk state are managed here:
|
|
5
|
-
*
|
|
6
|
-
* 1. **Channel selection** — `~/.pugi/config.json::updateChannel`.
|
|
7
|
-
* Persisted across sessions so `pugi update` keeps polling the
|
|
8
|
-
* same track the operator opted into via `pugi update --channel
|
|
9
|
-
* <name>`. Mirrors the read/write pattern used by
|
|
10
|
-
* `core/permissions/state.ts::getGlobalDefaultMode` (passthrough
|
|
11
|
-
* schema, atomic tmp+rename, defensive parse).
|
|
12
|
-
*
|
|
13
|
-
* 2. **Last-check timestamp** — `~/.pugi/.last-update-check` (ISO
|
|
14
|
-
* string, single-line). Read by the cold-start banner gate so
|
|
15
|
-
* operators only see the "update available" hint once per
|
|
16
|
-
* `UPDATE_CHECK_INTERVAL_HOURS` (default 24h). Living on its own
|
|
17
|
-
* file (NOT a JSON object inside config.json) is intentional:
|
|
18
|
-
* the timestamp is a hot path — every CLI invocation touches it —
|
|
19
|
-
* and a single-line read+write is materially faster than the
|
|
20
|
-
* JSON parse + serialise of the broader config doc, with no
|
|
21
|
-
* schema coupling cost.
|
|
22
|
-
*
|
|
23
|
-
* Module contract:
|
|
24
|
-
*
|
|
25
|
-
* - Every file path resolver accepts a `homeDir` override so the
|
|
26
|
-
* test suite can drive the module through a per-test mkdtemp
|
|
27
|
-
* directory without polluting the real `~/.pugi/`.
|
|
28
|
-
*
|
|
29
|
-
* - Parse / read helpers NEVER throw on a malformed file. A
|
|
30
|
-
* corrupted JSON blob, a missing field, or an unreadable file all
|
|
31
|
-
* collapse to "no persisted value" so the next layer (the CLI
|
|
32
|
-
* flag or the hard default `beta`) takes over. A future-self
|
|
33
|
-
* debugging an update flow against a corrupt config never has the
|
|
34
|
-
* CLI crash on them.
|
|
35
|
-
*
|
|
36
|
-
* - Write helpers use the atomic tmp+rename idiom so a kill mid-
|
|
37
|
-
* write never produces a half-flushed JSON document. The
|
|
38
|
-
* timestamp file is small enough that POSIX `rename` is itself
|
|
39
|
-
* atomic in practice, but we keep the idiom uniform with the
|
|
40
|
-
* config write so reviewers do not have to context-switch.
|
|
41
|
-
*/
|
|
42
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
43
|
-
import { homedir } from 'node:os';
|
|
44
|
-
import { resolve, dirname } from 'node:path';
|
|
45
|
-
import { z } from 'zod';
|
|
46
|
-
import { DEFAULT_UPDATE_CHANNEL, UPDATE_CHANNELS, } from './channels.js';
|
|
47
|
-
/**
|
|
48
|
-
* Default rate-limit window between registry probes. Operators see the
|
|
49
|
-
* cold-start banner at most once per window. Override per call via
|
|
50
|
-
* `shouldCheckForUpdate({ intervalHours })` — the cron-style scheduler
|
|
51
|
-
* passes 0 to force a check on every invocation, the doctor probe
|
|
52
|
-
* passes 24 to match the operator-visible cadence.
|
|
53
|
-
*/
|
|
54
|
-
export const UPDATE_CHECK_INTERVAL_HOURS = 24;
|
|
55
|
-
/** Filename of the per-user channel + misc config. Mirrors L6 / L25. */
|
|
56
|
-
const CONFIG_FILE = '.pugi/config.json';
|
|
57
|
-
/** Filename of the standalone last-check ISO timestamp. */
|
|
58
|
-
const LAST_CHECK_FILE = '.pugi/.last-update-check';
|
|
59
|
-
/**
|
|
60
|
-
* Zod schema for the channel slice of `~/.pugi/config.json`. The
|
|
61
|
-
* passthrough lets sibling skills (L6 `defaultPermissionMode`, L25
|
|
62
|
-
* onboarding marker, etc.) coexist in the same JSON document without
|
|
63
|
-
* dropping their fields on a channel write.
|
|
64
|
-
*/
|
|
65
|
-
const channelConfigSchema = z
|
|
66
|
-
.object({
|
|
67
|
-
updateChannel: z.enum(['stable', 'beta', 'canary']).optional(),
|
|
68
|
-
})
|
|
69
|
-
.partial()
|
|
70
|
-
.passthrough();
|
|
71
|
-
/**
|
|
72
|
-
* Resolve the absolute path of the per-user config file. Defaults to
|
|
73
|
-
* the real home dir, but every caller in the spec passes an explicit
|
|
74
|
-
* tmpdir so the persisted writes never escape the test sandbox.
|
|
75
|
-
*/
|
|
76
|
-
export function configPath(homeDir = homedir()) {
|
|
77
|
-
return resolve(homeDir, CONFIG_FILE);
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Resolve the absolute path of the single-line last-check file.
|
|
81
|
-
*/
|
|
82
|
-
export function lastCheckPath(homeDir = homedir()) {
|
|
83
|
-
return resolve(homeDir, LAST_CHECK_FILE);
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Read the persisted channel selection. Returns `null` when the
|
|
87
|
-
* config file is absent, the field is unset, or the file is unparse-
|
|
88
|
-
* able. The caller layers in the CLI flag + the hard default
|
|
89
|
-
* `DEFAULT_UPDATE_CHANNEL`.
|
|
90
|
-
*
|
|
91
|
-
* Defensive parse is intentional — a half-written config from a
|
|
92
|
-
* crashed session should never block `pugi update` from finishing the
|
|
93
|
-
* channel switch.
|
|
94
|
-
*/
|
|
95
|
-
export function getUpdateChannel(homeDir = homedir()) {
|
|
96
|
-
const path = configPath(homeDir);
|
|
97
|
-
if (!existsSync(path))
|
|
98
|
-
return null;
|
|
99
|
-
try {
|
|
100
|
-
const raw = readFileSync(path, 'utf8');
|
|
101
|
-
const parsed = channelConfigSchema.parse(JSON.parse(raw));
|
|
102
|
-
return parsed.updateChannel ?? null;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Resolve the effective channel for an invocation. Resolution order:
|
|
110
|
-
*
|
|
111
|
-
* 1. `cliFlag` (when provided + parses to a known channel).
|
|
112
|
-
* 2. `~/.pugi/config.json::updateChannel`.
|
|
113
|
-
* 3. `DEFAULT_UPDATE_CHANNEL` (currently `beta`).
|
|
114
|
-
*
|
|
115
|
-
* An invalid `cliFlag` (e.g. `--channel yolo`) falls through to the
|
|
116
|
-
* next layer rather than crashing — the dispatcher already validates
|
|
117
|
-
* the flag up front and surfaces a deterministic error for unknown
|
|
118
|
-
* names. This helper exists for code paths (the doctor probe, the
|
|
119
|
-
* cold-start banner) where no CLI flag is in play and a silent fall-
|
|
120
|
-
* through is the correct behaviour.
|
|
121
|
-
*/
|
|
122
|
-
export function resolveEffectiveChannel(options = {}) {
|
|
123
|
-
const cli = options.cliFlag;
|
|
124
|
-
if (cli && typeof cli === 'string') {
|
|
125
|
-
const trimmed = cli.trim().toLowerCase();
|
|
126
|
-
for (const channel of UPDATE_CHANNELS) {
|
|
127
|
-
if (channel === trimmed)
|
|
128
|
-
return channel;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const persisted = getUpdateChannel(options.homeDir ?? homedir());
|
|
132
|
-
if (persisted)
|
|
133
|
-
return persisted;
|
|
134
|
-
return DEFAULT_UPDATE_CHANNEL;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Persist the channel to `~/.pugi/config.json::updateChannel`. Creates
|
|
138
|
-
* `~/.pugi/` when missing; preserves any unrelated keys in the file
|
|
139
|
-
* (passthrough schema). Atomic tmp+rename so a kill mid-write never
|
|
140
|
-
* leaves the config half-flushed.
|
|
141
|
-
*/
|
|
142
|
-
export function setUpdateChannel(channel, homeDir = homedir()) {
|
|
143
|
-
const path = configPath(homeDir);
|
|
144
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
145
|
-
const existing = existsSync(path)
|
|
146
|
-
? safeParseObject(readFileSync(path, 'utf8'))
|
|
147
|
-
: {};
|
|
148
|
-
const next = { ...existing, updateChannel: channel };
|
|
149
|
-
const tmpPath = `${path}.tmp`;
|
|
150
|
-
writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, {
|
|
151
|
-
encoding: 'utf8',
|
|
152
|
-
mode: 0o600,
|
|
153
|
-
});
|
|
154
|
-
renameSync(tmpPath, path);
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Read the ISO timestamp of the most recent registry probe. Returns
|
|
158
|
-
* `null` when the file is absent or the contents do not parse as a
|
|
159
|
-
* valid Date. The caller treats `null` as "never checked" and runs an
|
|
160
|
-
* immediate probe.
|
|
161
|
-
*/
|
|
162
|
-
export function readLastCheckedAt(homeDir = homedir()) {
|
|
163
|
-
const path = lastCheckPath(homeDir);
|
|
164
|
-
if (!existsSync(path))
|
|
165
|
-
return null;
|
|
166
|
-
try {
|
|
167
|
-
const raw = readFileSync(path, 'utf8').trim();
|
|
168
|
-
if (raw.length === 0)
|
|
169
|
-
return null;
|
|
170
|
-
const ts = Date.parse(raw);
|
|
171
|
-
if (!Number.isFinite(ts))
|
|
172
|
-
return null;
|
|
173
|
-
return new Date(ts);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Persist the timestamp of the most recent registry probe. Atomic
|
|
181
|
-
* tmp+rename for the same reasons as `setUpdateChannel` — the file is
|
|
182
|
-
* small but we keep the idiom uniform.
|
|
183
|
-
*/
|
|
184
|
-
export function writeLastCheckedAt(when, homeDir = homedir()) {
|
|
185
|
-
const path = lastCheckPath(homeDir);
|
|
186
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
187
|
-
const tmpPath = `${path}.tmp`;
|
|
188
|
-
writeFileSync(tmpPath, `${when.toISOString()}\n`, {
|
|
189
|
-
encoding: 'utf8',
|
|
190
|
-
mode: 0o600,
|
|
191
|
-
});
|
|
192
|
-
renameSync(tmpPath, path);
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Decide whether the cold-start hint should run a fresh registry
|
|
196
|
-
* probe. Returns true when the last probe was more than
|
|
197
|
-
* `intervalHours` ago OR the timestamp file is missing entirely.
|
|
198
|
-
*
|
|
199
|
-
* Pass `intervalHours = 0` to force a probe on every call (used by
|
|
200
|
-
* the `pugi update --check` JSON surface where the operator is
|
|
201
|
-
* explicitly asking for a fresh result).
|
|
202
|
-
*/
|
|
203
|
-
export function shouldCheckForUpdate(options = {}) {
|
|
204
|
-
const now = options.now ? options.now() : Date.now();
|
|
205
|
-
const intervalHours = options.intervalHours ?? UPDATE_CHECK_INTERVAL_HOURS;
|
|
206
|
-
if (intervalHours <= 0)
|
|
207
|
-
return true;
|
|
208
|
-
const last = readLastCheckedAt(options.homeDir ?? homedir());
|
|
209
|
-
if (!last)
|
|
210
|
-
return true;
|
|
211
|
-
const ageMs = now - last.getTime();
|
|
212
|
-
const windowMs = intervalHours * 60 * 60 * 1_000;
|
|
213
|
-
return ageMs >= windowMs;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Defensive helper — parse JSON to an object; non-object payloads
|
|
217
|
-
* (top-level array, primitive) collapse to an empty object so the
|
|
218
|
-
* channel-write merge does not surface a TypeError. Mirrors the
|
|
219
|
-
* `safeParseObject` in `core/permissions/state.ts` — duplicating the
|
|
220
|
-
* 10 lines is cheaper than threading a shared util module through
|
|
221
|
-
* two unrelated leak surfaces.
|
|
222
|
-
*/
|
|
223
|
-
function safeParseObject(raw) {
|
|
224
|
-
try {
|
|
225
|
-
const parsed = JSON.parse(raw);
|
|
226
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
227
|
-
return parsed;
|
|
228
|
-
}
|
|
229
|
-
return {};
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
return {};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
//# sourceMappingURL=state.js.map
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* — `--bare` mode predicate.
|
|
3
|
-
*
|
|
4
|
-
* Mirror of the upstream tool's `--bare` flag: when active the CLI behaves
|
|
5
|
-
* like a plain LLM frontend with NO project auto-discovery. Useful for:
|
|
6
|
-
*
|
|
7
|
-
* - headless scripting where the operator wants deterministic, repo-
|
|
8
|
-
* independent behavior (`pugi --bare --print "..."`),
|
|
9
|
-
* - dropping into a workspace without auto-creating `.pugi/`,
|
|
10
|
-
* - REPL sessions that should NOT inject ambient `PUGI.md` / `CLAUDE.md`
|
|
11
|
-
* into the model prompt,
|
|
12
|
-
* - support / triage flows where the engineer needs the CLI to act
|
|
13
|
-
* like a fresh install regardless of where it's invoked.
|
|
14
|
-
*
|
|
15
|
-
* Discovery surfaces gated by `isBareMode()`:
|
|
16
|
-
*
|
|
17
|
-
* 1. `PUGI.md` / `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` parent-dir
|
|
18
|
-
* walk-up (`loadTraversedMarkdown` in `core/context/markdown-traverse.ts`).
|
|
19
|
-
* 2. Workspace-root markdown context (`loadMarkdownContext` consumers).
|
|
20
|
-
* 3. Auto-init `.pugi/` scaffold on REPL boot in untouched dirs.
|
|
21
|
-
* 4. Persona / skill auto-load from `.pugi/skills/`.
|
|
22
|
-
* 5. Workspace summary (`readPugiSummary`) read on REPL session start.
|
|
23
|
-
*
|
|
24
|
-
* Activation precedence — the bare bit is "sticky" once set so any
|
|
25
|
-
* subprocess the CLI spawns inherits it without re-passing the flag:
|
|
26
|
-
*
|
|
27
|
-
* 1. Top-level `--bare` arg parsed by `parseArgs` in `runtime/cli.ts`.
|
|
28
|
-
* The parser sets `process.env.PUGI_BARE='1'` BEFORE the dispatch
|
|
29
|
-
* flows so callsites checking the env see the activated state.
|
|
30
|
-
* 2. `PUGI_BARE=1` env var (any value matching `/^(1|true|yes|on)$/i`).
|
|
31
|
-
* 3. Default: bare mode OFF — full auto-discovery as before.
|
|
32
|
-
*
|
|
33
|
-
* This mirrors the existing `PUGI_SKIP_SPLASH` / `PUGI_NO_AUTO_INIT`
|
|
34
|
-
* env-flag pattern so the bare module fits the rest of the runtime
|
|
35
|
-
* configuration grammar without inventing a new wire.
|
|
36
|
-
*
|
|
37
|
-
* Test surface: `apps/pugi-cli/test/bare-mode.spec.ts` exercises the
|
|
38
|
-
* env precedence, value parsing, and the explicit-set / clear helpers.
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* Env var consulted by `isBareMode()`. Kept as an export so the spec
|
|
42
|
-
* + the runtime CLI can use the same constant — no string-typing of
|
|
43
|
-
* the wire name across modules.
|
|
44
|
-
*/
|
|
45
|
-
export const PUGI_BARE_ENV = 'PUGI_BARE';
|
|
46
|
-
/**
|
|
47
|
-
* Truthy values recognised on the `PUGI_BARE` env. Anything else
|
|
48
|
-
* (empty string, `0`, `false`, `no`, `off`, `disabled`, undefined) is
|
|
49
|
-
* treated as bare-mode OFF. The list is intentionally short — the
|
|
50
|
-
* value is set by the CLI parser and is not customer-typed prose, so
|
|
51
|
-
* we do not need a permissive boolean coercion.
|
|
52
|
-
*/
|
|
53
|
-
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
54
|
-
/**
|
|
55
|
-
* Return true when bare mode is active for the current process. Reads
|
|
56
|
-
* `process.env[PUGI_BARE_ENV]` and applies the truthy-value match.
|
|
57
|
-
*
|
|
58
|
-
* Safe to call from any module (no FS, no side-effects). The runtime
|
|
59
|
-
* cost is a single env-var lookup + lower-case + set membership, so
|
|
60
|
-
* gating hot-path callsites with `if (isBareMode()) return ...` adds
|
|
61
|
-
* effectively zero overhead in the default (non-bare) case.
|
|
62
|
-
*/
|
|
63
|
-
export function isBareMode(env = process.env) {
|
|
64
|
-
const raw = env[PUGI_BARE_ENV];
|
|
65
|
-
if (typeof raw !== 'string' || raw.length === 0)
|
|
66
|
-
return false;
|
|
67
|
-
return TRUTHY.has(raw.toLowerCase());
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Explicitly activate bare mode for the current process. Called by
|
|
71
|
-
* `parseArgs` in `runtime/cli.ts` when `--bare` is seen on the command
|
|
72
|
-
* line so downstream modules (engine, REPL bootstrap, doctor probe)
|
|
73
|
-
* see a consistent activated state via `isBareMode()` regardless of
|
|
74
|
-
* whether the operator set the env var manually or used the flag.
|
|
75
|
-
*
|
|
76
|
-
* Subprocess inheritance is the reason we mutate `process.env` rather
|
|
77
|
-
* than threading a `bare: boolean` field through every call signature
|
|
78
|
-
* — every Node child_process spawn inherits `process.env` by default,
|
|
79
|
-
* so the bare bit propagates to MCP servers / hook scripts / git
|
|
80
|
-
* subprocesses without ceremony.
|
|
81
|
-
*/
|
|
82
|
-
export function setBareMode(env = process.env) {
|
|
83
|
-
env[PUGI_BARE_ENV] = '1';
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Clear bare mode for the current process. Provided primarily for the
|
|
87
|
-
* spec so adjacent tests do not leak state between cases. Production
|
|
88
|
-
* code does NOT call this — bare mode is a one-shot per process.
|
|
89
|
-
*/
|
|
90
|
-
export function clearBareMode(env = process.env) {
|
|
91
|
-
delete env[PUGI_BARE_ENV];
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Human-readable one-line banner printed by the dispatcher when bare
|
|
95
|
-
* mode is active and the invocation is NOT JSON-only. Kept as a single
|
|
96
|
-
* constant so the spec can assert the exact wording and downstream
|
|
97
|
-
* tools (status bars, doctor row, REPL header) stay in lockstep.
|
|
98
|
-
*/
|
|
99
|
-
export const BARE_MODE_BANNER = 'Pugi --bare mode: project auto-discovery disabled.';
|
|
100
|
-
/**
|
|
101
|
-
* Short label rendered inside the `pugi doctor` table when bare mode
|
|
102
|
-
* is active. The doctor probe surfaces `BARE MODE` as a separate row
|
|
103
|
-
* so operators triaging "why is Pugi ignoring my PUGI.md" see the
|
|
104
|
-
* cause without grep'ing the env.
|
|
105
|
-
*/
|
|
106
|
-
export const BARE_MODE_DOCTOR_LABEL = 'BARE MODE';
|
|
107
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bash/redirect — stdout-redirect helper (Pugi backlog P2,
|
|
3
|
-
* Karpathy "log discipline" pattern).
|
|
4
|
-
*
|
|
5
|
-
* Long-running scripts (training loops, monorepo builds, agentic
|
|
6
|
-
* stdout dumps) routinely emit hundreds of MB of text. Feeding that
|
|
7
|
-
* back into the LLM's context window burns tokens for noise — the
|
|
8
|
-
* model only needs the path + the trailing N lines to know whether
|
|
9
|
-
* the run succeeded and, if it failed, where to start looking.
|
|
10
|
-
*
|
|
11
|
-
* The redirect pipeline:
|
|
12
|
-
* 1. Caller flips `redirect` on `BashToolInput` to opt in.
|
|
13
|
-
* 2. `resolveRedirectTarget` decides the on-disk path:
|
|
14
|
-
* - default: .pugi/runs/<sessionId>/bash-<commandHash>.log
|
|
15
|
-
* - override: workspace-relative path supplied by the caller
|
|
16
|
-
* Concurrent calls with the same hash dedupe through the
|
|
17
|
-
* filename's `tool-call-id` suffix so two parallel dispatches
|
|
18
|
-
* never share a target file.
|
|
19
|
-
* 3. The bash tool spawns the child with stdio piped to a write
|
|
20
|
-
* stream pointed at the resolved path (no in-memory buffering of
|
|
21
|
-
* stdout/stderr beyond Node's internal pipe).
|
|
22
|
-
* 4. After the child exits, `readTail` reads the last N lines from
|
|
23
|
-
* the log file and `buildRedirectEnvelope` constructs the
|
|
24
|
-
* model-facing payload: empty stdout/stderr, populated logPath,
|
|
25
|
-
* populated tail, `truncated: false`.
|
|
26
|
-
*
|
|
27
|
-
* Token-savings example (real-world ceiling):
|
|
28
|
-
* 100k-line `pnpm build` log = ~6.4 MB raw stdout.
|
|
29
|
-
* - Without redirect: truncated to 32 KB head = ~8k tokens, and the
|
|
30
|
-
* trailing error (where the bug actually lives) is invisible.
|
|
31
|
-
* - With redirect: empty stdout + 20-line tail = ~400 tokens, plus
|
|
32
|
-
* logPath the operator/agent can tail on demand.
|
|
33
|
-
*
|
|
34
|
-
* This module is intentionally framework-free — it only owns path
|
|
35
|
-
* resolution, file tail reading, and the envelope shape. The bash
|
|
36
|
-
* tool wires it into the spawn lifecycle.
|
|
37
|
-
*/
|
|
38
|
-
import { createHash } from 'node:crypto';
|
|
39
|
-
import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
40
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
41
|
-
/**
|
|
42
|
-
* Default tail size when the caller does not pin one. Mirrors the
|
|
43
|
-
* Karpathy training-loop convention (`tail -n 20` for the last
|
|
44
|
-
* checkpoint window).
|
|
45
|
-
*/
|
|
46
|
-
export const REDIRECT_DEFAULT_TAIL_LINES = 20;
|
|
47
|
-
/**
|
|
48
|
-
* Hard upper bound on tail size. The cap defends the context budget
|
|
49
|
-
* from a caller that flips redirect on but then asks for 100k tail
|
|
50
|
-
* lines — which would defeat the entire point of the redirect.
|
|
51
|
-
*/
|
|
52
|
-
export const REDIRECT_MAX_TAIL_LINES = 200;
|
|
53
|
-
/**
|
|
54
|
-
* Deterministically hash a command string into a short hex prefix so
|
|
55
|
-
* the default log filename is predictable. Collisions are not a
|
|
56
|
-
* security concern (the file is workspace-scoped and the tool-call-id
|
|
57
|
-
* suffix disambiguates concurrent calls) so SHA-256 → 10 hex chars is
|
|
58
|
-
* the same compromise the agent-progress writer uses.
|
|
59
|
-
*/
|
|
60
|
-
export function hashCommand(command) {
|
|
61
|
-
return createHash('sha256').update(command).digest('hex').slice(0, 10);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Validate and clamp the caller-supplied tail size. Negative / zero /
|
|
65
|
-
* NaN / >max values fall back to the default rather than throwing —
|
|
66
|
-
* the bash tool is the wrong layer to refuse a dispatch over a tail
|
|
67
|
-
* sizing bug.
|
|
68
|
-
*/
|
|
69
|
-
export function normalizeTailLines(value) {
|
|
70
|
-
if (typeof value !== 'number' ||
|
|
71
|
-
!Number.isFinite(value) ||
|
|
72
|
-
value <= 0) {
|
|
73
|
-
return REDIRECT_DEFAULT_TAIL_LINES;
|
|
74
|
-
}
|
|
75
|
-
return Math.min(Math.floor(value), REDIRECT_MAX_TAIL_LINES);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Resolve the workspace-relative target the caller asked for (or the
|
|
79
|
-
* tool default) into an absolute path. Throws on traversal escapes
|
|
80
|
-
* (`..` out of workspace) and absolute-path overrides — those are the
|
|
81
|
-
* same anti-patterns the existing `resolveWorkspacePath` rejects, but
|
|
82
|
-
* reimplemented locally so this helper has no cross-module coupling.
|
|
83
|
-
*/
|
|
84
|
-
export function resolveRedirectTarget(opts) {
|
|
85
|
-
const { workspaceRoot, sessionId, toolCallId, command, override } = opts;
|
|
86
|
-
const hash = hashCommand(command);
|
|
87
|
-
// Concurrent calls with the same command hash get distinct files by
|
|
88
|
-
// suffixing the (unique) tool-call-id. The bash tool already
|
|
89
|
-
// allocates a fresh id per dispatch, so this is sufficient even
|
|
90
|
-
// when two REPL panes fire the identical command at the same
|
|
91
|
-
// millisecond.
|
|
92
|
-
const defaultRelative = join('.pugi', 'runs', sessionId, `bash-${hash}-${toolCallId}.log`);
|
|
93
|
-
const requested = override ?? defaultRelative;
|
|
94
|
-
if (isAbsolute(requested)) {
|
|
95
|
-
throw new Error(`redirect.path must be workspace-relative; got absolute path: ${requested}`);
|
|
96
|
-
}
|
|
97
|
-
const absolutePath = resolve(workspaceRoot, requested);
|
|
98
|
-
const rel = relative(workspaceRoot, absolutePath);
|
|
99
|
-
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
100
|
-
throw new Error(`redirect.path escapes workspace root: ${requested}`);
|
|
101
|
-
}
|
|
102
|
-
// Derive the directory the bash tool needs to mkdir before opening
|
|
103
|
-
// the write stream. We split on the trailing separator rather than
|
|
104
|
-
// calling `dirname` so the helper has zero `path` API drift across
|
|
105
|
-
// platforms.
|
|
106
|
-
const slashIndex = Math.max(absolutePath.lastIndexOf('/'), absolutePath.lastIndexOf('\\'));
|
|
107
|
-
const directory = slashIndex > 0
|
|
108
|
-
? absolutePath.slice(0, slashIndex)
|
|
109
|
-
: workspaceRoot;
|
|
110
|
-
return {
|
|
111
|
-
absolutePath,
|
|
112
|
-
workspacePath: rel,
|
|
113
|
-
directory,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Ensure the redirect file is opened for write through a
|
|
118
|
-
* `tmp + rename` dance so a partial write (operator Ctrl+C, child
|
|
119
|
-
* crash mid-flush) never leaves a torn file at the target path.
|
|
120
|
-
*
|
|
121
|
-
* Returns the open file descriptor along with the temp path so the
|
|
122
|
-
* caller can rename-on-success and unlink-on-cancel.
|
|
123
|
-
*
|
|
124
|
-
* The temp path mirrors the existing `editTool`/`writeTool` pattern:
|
|
125
|
-
* append `.pugi-tmp-<ts>` to the final name. We use `openSync` (vs
|
|
126
|
-
* `createWriteStream`) so the bash tool can hand the fd directly to
|
|
127
|
-
* `spawn`'s `stdio` array.
|
|
128
|
-
*/
|
|
129
|
-
export function openRedirectFile(target) {
|
|
130
|
-
mkdirSync(target.directory, { recursive: true });
|
|
131
|
-
const tempPath = `${target.absolutePath}.pugi-tmp-${Date.now()}-${process.pid}`;
|
|
132
|
-
// mode 0o600 = rw for owner only — log files may contain secrets
|
|
133
|
-
// the child process leaked to stdout, so we lock them down by
|
|
134
|
-
// default. The bash tool can later promote them to 0o644 on a
|
|
135
|
-
// per-call basis when the operator opts in.
|
|
136
|
-
const fd = openSync(tempPath, 'w', 0o600);
|
|
137
|
-
return { fd, tempPath };
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Finalise the temp file by renaming it onto `absolutePath` so the
|
|
141
|
-
* model-facing logPath always points at a complete file. Idempotent —
|
|
142
|
-
* the bash tool calls this on success AND on timeout/cancel so the
|
|
143
|
-
* partial log is preserved as a debugging artifact (the agent can
|
|
144
|
-
* still read the tail to see how far the run got).
|
|
145
|
-
*/
|
|
146
|
-
export function finaliseRedirectFile(target, tempPath) {
|
|
147
|
-
if (!existsSync(tempPath))
|
|
148
|
-
return;
|
|
149
|
-
renameSync(tempPath, target.absolutePath);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Best-effort cleanup of the temp file when the redirect path was
|
|
153
|
-
* never fully written (e.g. spawn never started). Swallows ENOENT so
|
|
154
|
-
* the cleanup is idempotent.
|
|
155
|
-
*/
|
|
156
|
-
export function cleanupRedirectTemp(tempPath) {
|
|
157
|
-
try {
|
|
158
|
-
if (existsSync(tempPath))
|
|
159
|
-
unlinkSync(tempPath);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
// best-effort
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Read the last N lines of a file, without slurping the whole file
|
|
167
|
-
* into memory. We use a backwards-chunked read so even multi-GB logs
|
|
168
|
-
* stay bounded by the tail size.
|
|
169
|
-
*
|
|
170
|
-
* The implementation is intentionally simple — we read at most
|
|
171
|
-
* `maxBytes` (~64 KB by default) from the file tail because 200 lines
|
|
172
|
-
* of compiler output rarely exceed that. Long-line cases (one massive
|
|
173
|
-
* JSON blob on a single line) fall back to "whatever fits in maxBytes
|
|
174
|
-
* counts as the tail".
|
|
175
|
-
*/
|
|
176
|
-
export function readTail(path, lines, options = {}) {
|
|
177
|
-
if (!existsSync(path))
|
|
178
|
-
return '';
|
|
179
|
-
const maxBytes = options.maxBytes ?? 64 * 1024;
|
|
180
|
-
const stat = statSync(path);
|
|
181
|
-
if (stat.size === 0)
|
|
182
|
-
return '';
|
|
183
|
-
const readSize = Math.min(stat.size, maxBytes);
|
|
184
|
-
// `readFileSync` with an explicit byte slice is the simplest API
|
|
185
|
-
// available in node:fs without dropping to `read(fd, buf, ...)`.
|
|
186
|
-
// We read the trailing window of the file by opening it and
|
|
187
|
-
// seeking to `stat.size - readSize`.
|
|
188
|
-
const fd = openSync(path, 'r');
|
|
189
|
-
try {
|
|
190
|
-
const buf = Buffer.alloc(readSize);
|
|
191
|
-
const bytesRead = readFileSyncSlice(fd, buf, stat.size - readSize);
|
|
192
|
-
const trailing = buf.subarray(0, bytesRead).toString('utf8');
|
|
193
|
-
// The first line in the buffer may be a partial line if we did
|
|
194
|
-
// not seek to a line boundary. Drop it unless the buffer covers
|
|
195
|
-
// the whole file.
|
|
196
|
-
const startedFromHead = stat.size <= readSize;
|
|
197
|
-
const split = trailing.split('\n');
|
|
198
|
-
// Re-emit the tail; if we DID start mid-line, discard the
|
|
199
|
-
// partial-leading fragment.
|
|
200
|
-
const usable = startedFromHead ? split : split.slice(1);
|
|
201
|
-
// Trailing newline produces an empty string at the end of split;
|
|
202
|
-
// drop it so the line count matches operator expectation.
|
|
203
|
-
if (usable.length > 0 && usable[usable.length - 1] === '') {
|
|
204
|
-
usable.pop();
|
|
205
|
-
}
|
|
206
|
-
return usable.slice(-lines).join('\n');
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
closeSync(fd);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Helper: read `buf.length` bytes from `fd` starting at `position`.
|
|
214
|
-
* Returns the actual number of bytes read.
|
|
215
|
-
*
|
|
216
|
-
* Loops over `readSync` so a short read on a large file (interruptible
|
|
217
|
-
* syscall, EAGAIN) still produces the full requested window. Bounded
|
|
218
|
-
* by `buf.length` so the helper cannot over-read past the caller's
|
|
219
|
-
* buffer regardless of how the kernel chunked the response.
|
|
220
|
-
*/
|
|
221
|
-
function readFileSyncSlice(fd, buf, position) {
|
|
222
|
-
let totalRead = 0;
|
|
223
|
-
while (totalRead < buf.length) {
|
|
224
|
-
const got = readSync(fd, buf, totalRead, buf.length - totalRead, position + totalRead);
|
|
225
|
-
if (got === 0)
|
|
226
|
-
break;
|
|
227
|
-
totalRead += got;
|
|
228
|
-
}
|
|
229
|
-
return totalRead;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Compose the model-facing envelope. The bash tool calls this once
|
|
233
|
-
* the child has exited and the log file has been renamed into place.
|
|
234
|
-
*
|
|
235
|
-
* `truncated` is hardwired to `false` because the redirect contract
|
|
236
|
-
* is "the full output is in the log, we hand you the path and a
|
|
237
|
-
* tail". The legacy stdout/stderr truncation marker (`(...truncated
|
|
238
|
-
* at N bytes)`) only fires in the buffered path that this redirect
|
|
239
|
-
* pipeline replaces — surfacing `truncated: true` for a redirect
|
|
240
|
-
* would mislead the model into thinking it lost data.
|
|
241
|
-
*/
|
|
242
|
-
export function buildRedirectEnvelope(opts) {
|
|
243
|
-
return {
|
|
244
|
-
stdout: '',
|
|
245
|
-
stderr: '',
|
|
246
|
-
exitCode: opts.exitCode,
|
|
247
|
-
logPath: opts.logPath,
|
|
248
|
-
tail: opts.tail,
|
|
249
|
-
truncated: false,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Convenience wrapper for the bash tool: given a completed spawn
|
|
254
|
-
* (exit code + finalised log path) and a caller-supplied
|
|
255
|
-
* `BashRedirectOptions`, build the envelope in one call. The bash
|
|
256
|
-
* tool keeps the spawn lifecycle (open fd, stream child's
|
|
257
|
-
* stdout/stderr to it, wait for close, rename) but delegates the
|
|
258
|
-
* tail-read + envelope assembly here so the test suite can exercise
|
|
259
|
-
* the helper without spinning up a child process.
|
|
260
|
-
*/
|
|
261
|
-
export function applyRedirect(opts) {
|
|
262
|
-
const tail = readTail(opts.target.absolutePath, opts.tailLines);
|
|
263
|
-
return buildRedirectEnvelope({
|
|
264
|
-
exitCode: opts.exitCode,
|
|
265
|
-
logPath: opts.target.workspacePath,
|
|
266
|
-
tail,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Internal helper used by the spec when it needs to seed a log file
|
|
271
|
-
* for the tail-reader. Exported so tests do not depend on the
|
|
272
|
-
* file-tools/write surface (which has its own permission gate).
|
|
273
|
-
*/
|
|
274
|
-
export function writeLogForTest(absolutePath, body) {
|
|
275
|
-
const slashIndex = Math.max(absolutePath.lastIndexOf('/'), absolutePath.lastIndexOf('\\'));
|
|
276
|
-
if (slashIndex > 0) {
|
|
277
|
-
mkdirSync(absolutePath.slice(0, slashIndex), { recursive: true });
|
|
278
|
-
}
|
|
279
|
-
writeFileSync(absolutePath, body, { encoding: 'utf8', mode: 0o600 });
|
|
280
|
-
}
|
|
281
|
-
//# sourceMappingURL=redirect.js.map
|