@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,401 +0,0 @@
|
|
|
1
|
-
export class MarkerParseError extends Error {
|
|
2
|
-
code = 'MARKER_PARSE_ERROR';
|
|
3
|
-
modelHint;
|
|
4
|
-
atLine;
|
|
5
|
-
constructor(message, modelHint, atLine) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.name = 'MarkerParseError';
|
|
8
|
-
this.modelHint = modelHint;
|
|
9
|
-
this.atLine = atLine;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Convert a raw LLM response into parsed edits. Marker family routes
|
|
14
|
-
* by tag; `auto` triggers detection.
|
|
15
|
-
*
|
|
16
|
-
* Layer C/D markers ALWAYS use the universal `@@@` envelope (vendor-
|
|
17
|
-
* neutral) because their wire format is heavy enough that re-emitting
|
|
18
|
-
* per-vendor would invite drift. Layer A/B follow the per-vendor
|
|
19
|
-
* SEARCH/REPLACE convention.
|
|
20
|
-
*/
|
|
21
|
-
export function parseMarkers(raw, family) {
|
|
22
|
-
if (typeof raw !== 'string') {
|
|
23
|
-
throw new MarkerParseError('parseMarkers requires a string input', family);
|
|
24
|
-
}
|
|
25
|
-
const effective = family === 'auto' ? detectFamily(raw) : family;
|
|
26
|
-
// Layer C and Layer D envelopes are vendor-neutral and trimmed out
|
|
27
|
-
// before per-vendor Layer A/B parsing so the per-vendor pass cannot
|
|
28
|
-
// accidentally tokenise the rewrite/AST body.
|
|
29
|
-
const universalEdits = [];
|
|
30
|
-
const stripped = extractUniversalBlocks(raw, universalEdits, family);
|
|
31
|
-
const familyEdits = parseFamilyMarkers(stripped, effective);
|
|
32
|
-
return [...universalEdits, ...familyEdits];
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Heuristic auto-detection — looks for the first distinctive marker
|
|
36
|
-
* and routes accordingly. Cheap to compute (single linear scan via
|
|
37
|
-
* `indexOf`) so we run it whenever the dispatcher gets `modelTag:
|
|
38
|
-
* undefined`.
|
|
39
|
-
*/
|
|
40
|
-
export function detectFamily(raw) {
|
|
41
|
-
// Order matters: Gemini's `<<<<<<< SEARCH` is distinctive enough to
|
|
42
|
-
// win even when the same payload contains a unified diff in a
|
|
43
|
-
// comment block.
|
|
44
|
-
if (raw.includes('<<<<<<< SEARCH'))
|
|
45
|
-
return 'gemini';
|
|
46
|
-
if (raw.includes('+++ NEW') && raw.includes('--- OLD'))
|
|
47
|
-
return 'anthropic';
|
|
48
|
-
// Unified-diff signature: `--- a/` immediately followed by `+++ b/`
|
|
49
|
-
// somewhere in the payload. Looser than Anthropic's `+++ NEW`.
|
|
50
|
-
if (/^--- a\//m.test(raw) && /^\+\+\+ b\//m.test(raw))
|
|
51
|
-
return 'openai';
|
|
52
|
-
// Default to Anthropic — covers the Claude family which is the
|
|
53
|
-
// primary alpha target.
|
|
54
|
-
return 'anthropic';
|
|
55
|
-
}
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Universal Layer C / Layer D envelope extractor.
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
function extractUniversalBlocks(raw, out, family) {
|
|
60
|
-
const lines = raw.split('\n');
|
|
61
|
-
const kept = [];
|
|
62
|
-
let i = 0;
|
|
63
|
-
while (i < lines.length) {
|
|
64
|
-
const line = lines[i] ?? '';
|
|
65
|
-
const rewrite = line.match(/^@@@ REWRITE\s+(\S+)\s+sha256=([a-f0-9]+)\s*$/);
|
|
66
|
-
if (rewrite) {
|
|
67
|
-
const file = rewrite[1] ?? '';
|
|
68
|
-
const baseSha256 = (rewrite[2] ?? '').toLowerCase();
|
|
69
|
-
const bodyStart = i + 1;
|
|
70
|
-
let bodyEnd = bodyStart;
|
|
71
|
-
while (bodyEnd < lines.length && lines[bodyEnd] !== '@@@ END')
|
|
72
|
-
bodyEnd += 1;
|
|
73
|
-
if (bodyEnd >= lines.length) {
|
|
74
|
-
throw new MarkerParseError(`Layer C REWRITE block for ${file} missing '@@@ END' terminator`, family, i + 1);
|
|
75
|
-
}
|
|
76
|
-
const newContents = lines.slice(bodyStart, bodyEnd).join('\n');
|
|
77
|
-
out.push({
|
|
78
|
-
kind: 'layer-c',
|
|
79
|
-
edit: { file, baseSha256, newContents },
|
|
80
|
-
});
|
|
81
|
-
i = bodyEnd + 1;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const ast = line.match(/^@@@ AST\s+(\S+)\s+op=(\S+)\s*$/);
|
|
85
|
-
if (ast) {
|
|
86
|
-
const file = ast[1] ?? '';
|
|
87
|
-
const operation = (ast[2] ?? '');
|
|
88
|
-
const bodyStart = i + 1;
|
|
89
|
-
let bodyEnd = bodyStart;
|
|
90
|
-
while (bodyEnd < lines.length && lines[bodyEnd] !== '@@@ END')
|
|
91
|
-
bodyEnd += 1;
|
|
92
|
-
if (bodyEnd >= lines.length) {
|
|
93
|
-
throw new MarkerParseError(`Layer D AST block for ${file} missing '@@@ END' terminator`, family, i + 1);
|
|
94
|
-
}
|
|
95
|
-
const paramsRaw = lines.slice(bodyStart, bodyEnd).join('\n').trim();
|
|
96
|
-
let params = {};
|
|
97
|
-
if (paramsRaw.length > 0) {
|
|
98
|
-
try {
|
|
99
|
-
const parsed = JSON.parse(paramsRaw);
|
|
100
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
101
|
-
params = parsed;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
throw new MarkerParseError(`Layer D AST params JSON parse failed for ${file}: ${error instanceof Error ? error.message : String(error)}`, family, bodyStart + 1);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
out.push({
|
|
109
|
-
kind: 'layer-d',
|
|
110
|
-
edit: { file, operation, params },
|
|
111
|
-
});
|
|
112
|
-
i = bodyEnd + 1;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
kept.push(line);
|
|
116
|
-
i += 1;
|
|
117
|
-
}
|
|
118
|
-
return kept.join('\n');
|
|
119
|
-
}
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Per-family Layer A / Layer B parsers.
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
function parseFamilyMarkers(raw, family) {
|
|
124
|
-
switch (family) {
|
|
125
|
-
case 'anthropic':
|
|
126
|
-
return parseAnthropic(raw);
|
|
127
|
-
case 'gemini':
|
|
128
|
-
return parseGemini(raw);
|
|
129
|
-
case 'openai':
|
|
130
|
-
return parseOpenAI(raw);
|
|
131
|
-
case 'auto':
|
|
132
|
-
// Unreachable — `parseMarkers` resolves `auto` to a concrete
|
|
133
|
-
// family before calling this. We keep the case so the switch is
|
|
134
|
-
// exhaustive under `noFallthroughCasesInSwitch` / `noImplicitReturns`.
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Anthropic / Claude — Cline three-segment envelope:
|
|
140
|
-
* +++ NEW <file>
|
|
141
|
-
* <new>
|
|
142
|
-
* --- OLD <file>
|
|
143
|
-
* <old>
|
|
144
|
-
* ===
|
|
145
|
-
*
|
|
146
|
-
* Multiple blocks targeting the same `<file>` collapse into a single
|
|
147
|
-
* Layer B batch (preserving order). Single blocks return as Layer A.
|
|
148
|
-
*/
|
|
149
|
-
// Shared between the body-terminator scan and the header-match check
|
|
150
|
-
// below so the two cannot drift. Triple-review P1 (Claude):
|
|
151
|
-
// the previous implementation used `lines[i].startsWith('--- OLD')`
|
|
152
|
-
// for the terminator and the regex below only for the header, so a
|
|
153
|
-
// model-emitted line like `// --- OLD usage was foo` inside the NEW
|
|
154
|
-
// body would falsely terminate the body.
|
|
155
|
-
const ANTHROPIC_OLD_HEADER = /^--- OLD\s+(\S+)\s*$/;
|
|
156
|
-
function parseAnthropic(raw) {
|
|
157
|
-
const blocks = [];
|
|
158
|
-
const lines = raw.split('\n');
|
|
159
|
-
let i = 0;
|
|
160
|
-
while (i < lines.length) {
|
|
161
|
-
const line = lines[i] ?? '';
|
|
162
|
-
const newHeader = line.match(/^\+\+\+ NEW\s+(\S+)\s*$/);
|
|
163
|
-
if (!newHeader) {
|
|
164
|
-
i += 1;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const file = newHeader[1] ?? '';
|
|
168
|
-
const startedAt = i + 1;
|
|
169
|
-
const newBody = [];
|
|
170
|
-
i += 1;
|
|
171
|
-
// Body terminator MUST match the same exact regex as `oldHeader`
|
|
172
|
-
// below (`/^--- OLD\s+\S+$/`), not a loose `startsWith('--- OLD')`.
|
|
173
|
-
// Triple-review P1 (Claude): a NEW body that contains a
|
|
174
|
-
// comment line beginning with `--- OLD foo` (e.g. a git-diff
|
|
175
|
-
// excerpt in a TypeScript comment, a markdown code fence showing
|
|
176
|
-
// what an old patch looked like) would falsely terminate the body
|
|
177
|
-
// and rip the rest of the NEW segment into the OLD segment, then
|
|
178
|
-
// throw a misleading "file mismatch" or write half-edits to disk.
|
|
179
|
-
while (i < lines.length && !ANTHROPIC_OLD_HEADER.test(lines[i] ?? '')) {
|
|
180
|
-
newBody.push(lines[i] ?? '');
|
|
181
|
-
i += 1;
|
|
182
|
-
}
|
|
183
|
-
if (i >= lines.length) {
|
|
184
|
-
throw new MarkerParseError(`Anthropic block for ${file} missing '--- OLD' segment`, 'anthropic', startedAt);
|
|
185
|
-
}
|
|
186
|
-
const oldHeader = (lines[i] ?? '').match(ANTHROPIC_OLD_HEADER);
|
|
187
|
-
if (!oldHeader || oldHeader[1] !== file) {
|
|
188
|
-
throw new MarkerParseError(`Anthropic block file mismatch: NEW ${file} vs OLD ${oldHeader?.[1] ?? '?'}`, 'anthropic', i + 1);
|
|
189
|
-
}
|
|
190
|
-
i += 1;
|
|
191
|
-
const oldBody = [];
|
|
192
|
-
while (i < lines.length && (lines[i] ?? '') !== '===') {
|
|
193
|
-
oldBody.push(lines[i] ?? '');
|
|
194
|
-
i += 1;
|
|
195
|
-
}
|
|
196
|
-
if (i >= lines.length) {
|
|
197
|
-
throw new MarkerParseError(`Anthropic block for ${file} missing '===' terminator`, 'anthropic', startedAt);
|
|
198
|
-
}
|
|
199
|
-
// Anthropic emits the bodies WITHOUT trailing newlines on the
|
|
200
|
-
// last line; our `.join('\n')` produces the same shape so a
|
|
201
|
-
// round-trip is byte-for-byte stable.
|
|
202
|
-
blocks.push({
|
|
203
|
-
file,
|
|
204
|
-
newString: newBody.join('\n'),
|
|
205
|
-
oldString: oldBody.join('\n'),
|
|
206
|
-
atLine: startedAt,
|
|
207
|
-
});
|
|
208
|
-
i += 1; // consume '==='
|
|
209
|
-
}
|
|
210
|
-
return collapseByFile(blocks);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Gemini / xAI — GitHub-conflict-marker envelope:
|
|
214
|
-
* <<<<<<< SEARCH [file]
|
|
215
|
-
* <old>
|
|
216
|
-
* =======
|
|
217
|
-
* <new>
|
|
218
|
-
* >>>>>>> REPLACE [file]
|
|
219
|
-
*
|
|
220
|
-
* `[file]` may appear on the SEARCH line, the REPLACE line, or BOTH.
|
|
221
|
-
* If both are present they must match.
|
|
222
|
-
*/
|
|
223
|
-
function parseGemini(raw) {
|
|
224
|
-
const blocks = [];
|
|
225
|
-
const lines = raw.split('\n');
|
|
226
|
-
let i = 0;
|
|
227
|
-
while (i < lines.length) {
|
|
228
|
-
const line = lines[i] ?? '';
|
|
229
|
-
const search = line.match(/^<<<<<<< SEARCH(?:\s+(\S+))?\s*$/);
|
|
230
|
-
if (!search) {
|
|
231
|
-
i += 1;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
const startedAt = i + 1;
|
|
235
|
-
const searchFile = search[1] ?? '';
|
|
236
|
-
i += 1;
|
|
237
|
-
const oldBody = [];
|
|
238
|
-
while (i < lines.length && (lines[i] ?? '') !== '=======') {
|
|
239
|
-
oldBody.push(lines[i] ?? '');
|
|
240
|
-
i += 1;
|
|
241
|
-
}
|
|
242
|
-
if (i >= lines.length) {
|
|
243
|
-
throw new MarkerParseError(`Gemini SEARCH block missing '=======' divider`, 'gemini', startedAt);
|
|
244
|
-
}
|
|
245
|
-
i += 1; // consume '======='
|
|
246
|
-
const newBody = [];
|
|
247
|
-
while (i < lines.length && !(lines[i] ?? '').startsWith('>>>>>>> REPLACE')) {
|
|
248
|
-
newBody.push(lines[i] ?? '');
|
|
249
|
-
i += 1;
|
|
250
|
-
}
|
|
251
|
-
if (i >= lines.length) {
|
|
252
|
-
throw new MarkerParseError(`Gemini SEARCH block missing '>>>>>>> REPLACE' terminator`, 'gemini', startedAt);
|
|
253
|
-
}
|
|
254
|
-
const replace = (lines[i] ?? '').match(/^>>>>>>> REPLACE(?:\s+(\S+))?\s*$/);
|
|
255
|
-
const replaceFile = replace?.[1] ?? '';
|
|
256
|
-
const file = searchFile || replaceFile;
|
|
257
|
-
if (!file) {
|
|
258
|
-
throw new MarkerParseError(`Gemini block missing file path on both SEARCH and REPLACE lines`, 'gemini', startedAt);
|
|
259
|
-
}
|
|
260
|
-
if (searchFile && replaceFile && searchFile !== replaceFile) {
|
|
261
|
-
throw new MarkerParseError(`Gemini SEARCH/REPLACE file mismatch: ${searchFile} vs ${replaceFile}`, 'gemini', i + 1);
|
|
262
|
-
}
|
|
263
|
-
i += 1; // consume REPLACE line
|
|
264
|
-
blocks.push({
|
|
265
|
-
file,
|
|
266
|
-
oldString: oldBody.join('\n'),
|
|
267
|
-
newString: newBody.join('\n'),
|
|
268
|
-
atLine: startedAt,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
return collapseByFile(blocks);
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* OpenAI / generic — unified diff. We parse a single contiguous hunk
|
|
275
|
-
* per file into a Layer A block by collapsing all `-` lines into
|
|
276
|
-
* oldString and all `+` lines into newString, preserving leading
|
|
277
|
-
* context. Multi-hunk files become a Layer B batch (one sub-edit per
|
|
278
|
-
* hunk). The grammar is intentionally minimal — Pugi does not need
|
|
279
|
-
* full GNU diff compatibility because the model owns the format
|
|
280
|
-
* choice.
|
|
281
|
-
*/
|
|
282
|
-
function parseOpenAI(raw) {
|
|
283
|
-
const blocks = [];
|
|
284
|
-
const lines = raw.split('\n');
|
|
285
|
-
let i = 0;
|
|
286
|
-
let currentFile = null;
|
|
287
|
-
while (i < lines.length) {
|
|
288
|
-
const line = lines[i] ?? '';
|
|
289
|
-
const aHeader = line.match(/^--- a\/(.+)$/);
|
|
290
|
-
if (aHeader) {
|
|
291
|
-
// Expect the matching `+++ b/<file>` on the next line.
|
|
292
|
-
const next = lines[i + 1] ?? '';
|
|
293
|
-
const bHeader = next.match(/^\+\+\+ b\/(.+)$/);
|
|
294
|
-
if (!bHeader) {
|
|
295
|
-
throw new MarkerParseError(`OpenAI unified diff: '--- a/${aHeader[1]}' not followed by '+++ b/'`, 'openai', i + 1);
|
|
296
|
-
}
|
|
297
|
-
if (aHeader[1] !== bHeader[1]) {
|
|
298
|
-
throw new MarkerParseError(`OpenAI unified diff file mismatch: a/${aHeader[1]} vs b/${bHeader[1]}`, 'openai', i + 2);
|
|
299
|
-
}
|
|
300
|
-
currentFile = bHeader[1] ?? '';
|
|
301
|
-
i += 2;
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
if (line.startsWith('@@') && currentFile) {
|
|
305
|
-
const startedAt = i + 1;
|
|
306
|
-
i += 1;
|
|
307
|
-
const oldLines = [];
|
|
308
|
-
const newLines = [];
|
|
309
|
-
while (i < lines.length &&
|
|
310
|
-
!(lines[i] ?? '').startsWith('@@') &&
|
|
311
|
-
!(lines[i] ?? '').startsWith('--- a/')) {
|
|
312
|
-
const hl = lines[i] ?? '';
|
|
313
|
-
if (hl.startsWith('+'))
|
|
314
|
-
newLines.push(hl.slice(1));
|
|
315
|
-
else if (hl.startsWith('-'))
|
|
316
|
-
oldLines.push(hl.slice(1));
|
|
317
|
-
else if (hl.startsWith(' ')) {
|
|
318
|
-
// Context line — preserved in both sides to keep the
|
|
319
|
-
// oldString unique. Strip the leading space.
|
|
320
|
-
const ctx = hl.slice(1);
|
|
321
|
-
oldLines.push(ctx);
|
|
322
|
-
newLines.push(ctx);
|
|
323
|
-
}
|
|
324
|
-
else if (hl === '') {
|
|
325
|
-
// Bare empty lines in a unified diff are ambiguous — some
|
|
326
|
-
// emitters use them as a terminator, others as a literal
|
|
327
|
-
// empty context line. We treat them as context (preserved
|
|
328
|
-
// on both sides) which is the conservative choice; if the
|
|
329
|
-
// model meant terminator the next `@@` or `--- a/` will
|
|
330
|
-
// catch it.
|
|
331
|
-
oldLines.push('');
|
|
332
|
-
newLines.push('');
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
// `\` lines ('') and any other
|
|
336
|
-
// metadata are ignored.
|
|
337
|
-
}
|
|
338
|
-
i += 1;
|
|
339
|
-
}
|
|
340
|
-
blocks.push({
|
|
341
|
-
file: currentFile,
|
|
342
|
-
oldString: oldLines.join('\n'),
|
|
343
|
-
newString: newLines.join('\n'),
|
|
344
|
-
atLine: startedAt,
|
|
345
|
-
});
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
i += 1;
|
|
349
|
-
}
|
|
350
|
-
if (blocks.length === 0 && raw.includes('--- a/')) {
|
|
351
|
-
throw new MarkerParseError('OpenAI unified diff parsed zero hunks — payload may be malformed', 'openai');
|
|
352
|
-
}
|
|
353
|
-
return collapseByFile(blocks);
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Collapse N blocks-per-file into either a single Layer A (N=1) or a
|
|
357
|
-
* single Layer B batch (N>1). The dispatcher routes Layer A and
|
|
358
|
-
* Layer B differently; we make that choice at parse time so the
|
|
359
|
-
* dispatcher stays format-agnostic.
|
|
360
|
-
*/
|
|
361
|
-
function collapseByFile(blocks) {
|
|
362
|
-
const byFile = new Map();
|
|
363
|
-
// Preserve first-seen order across files.
|
|
364
|
-
const order = [];
|
|
365
|
-
for (const block of blocks) {
|
|
366
|
-
const list = byFile.get(block.file);
|
|
367
|
-
if (list) {
|
|
368
|
-
list.push({ oldString: block.oldString, newString: block.newString });
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
byFile.set(block.file, [{ oldString: block.oldString, newString: block.newString }]);
|
|
372
|
-
order.push(block.file);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
const out = [];
|
|
376
|
-
for (const file of order) {
|
|
377
|
-
const subs = byFile.get(file) ?? [];
|
|
378
|
-
if (subs.length === 1) {
|
|
379
|
-
const only = subs[0];
|
|
380
|
-
out.push({
|
|
381
|
-
kind: 'layer-a',
|
|
382
|
-
edit: {
|
|
383
|
-
file,
|
|
384
|
-
oldString: only.oldString,
|
|
385
|
-
newString: only.newString,
|
|
386
|
-
},
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
out.push({
|
|
391
|
-
kind: 'layer-b',
|
|
392
|
-
edit: {
|
|
393
|
-
file,
|
|
394
|
-
edits: subs,
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return out;
|
|
400
|
-
}
|
|
401
|
-
//# sourceMappingURL=marker-parser.js.map
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared security gate for the diff escalation Layer A/B/C
|
|
3
|
-
* applicators.
|
|
4
|
-
*
|
|
5
|
-
* Before (triple-review remediation) every layer
|
|
6
|
-
* built its own path resolver via `isAbsolute(file) ? file : resolve(cwd, file)`.
|
|
7
|
-
* That bypassed the workspace-scoped resolver + protected-basename
|
|
8
|
-
* gate + symlink-escape check used by the `edit`/`write` tools in
|
|
9
|
-
* `apps/pugi-cli/src/tools/file-tools.ts`. A model that emitted
|
|
10
|
-
* `+++ NEW ../../../../etc/passwd` (or `+++ NEW .env`) would have
|
|
11
|
-
* happily landed an atomic write outside the workspace or onto a
|
|
12
|
-
* protected basename.
|
|
13
|
-
*
|
|
14
|
-
* This module is the single chokepoint every layer goes through.
|
|
15
|
-
* Centralised here so:
|
|
16
|
-
*
|
|
17
|
-
* 1. Future layers (D = AST, planned β-tier) inherit the gate for
|
|
18
|
-
* free instead of re-implementing it.
|
|
19
|
-
* 2. New protected-file rules added to `decidePermission` propagate
|
|
20
|
-
* to dispatcher writes uniformly.
|
|
21
|
-
* 3. A single spec surface covers every layer's path-handling
|
|
22
|
-
* contract (`apps/pugi-cli/test/edits-security-gate.spec.ts`).
|
|
23
|
-
*
|
|
24
|
-
* Defense layers, in order (fail-fast — first reject wins):
|
|
25
|
-
*
|
|
26
|
-
* a. `resolveWorkspacePath` — workspace-scoped resolver with URL-decode
|
|
27
|
-
* and realpath gate at the target. Throws on traversal; we
|
|
28
|
-
* translate the throw into `path_outside_workspace`.
|
|
29
|
-
* b. `decidePermission` (kind: 'edit') — protected basename / suffix /
|
|
30
|
-
* credential-path check (`.env`, `*.pem`, `id_rsa`, `~/.ssh/**`,
|
|
31
|
-
* etc.). Mirrors what `writeTool` / `editTool` already enforce.
|
|
32
|
-
* c. Explicit `realpathSync.native` re-check on the target (or
|
|
33
|
-
* target's parent when the target does not yet exist) to defend
|
|
34
|
-
* against TOCTOU symlink swap between `resolveWorkspacePath` and
|
|
35
|
-
* the apply-time write.
|
|
36
|
-
* d. Re-run `decidePermission` on the REAL target's workspace-relative
|
|
37
|
-
* form. R2 triple-review (2026-05-25, Codex P1) caught the
|
|
38
|
-
* symlink → protected-file bypass: a workspace-local symlink
|
|
39
|
-
* `alias -> .env` passes step (b) because `alias` is not a
|
|
40
|
-
* protected basename, and passes step (c) because `.env`
|
|
41
|
-
* legitimately lives inside the workspace realpath. Without step
|
|
42
|
-
* (d) the gate would allow the atomic write to land on `.env`
|
|
43
|
-
* via the symlink's resolved target. Re-running the protected-file
|
|
44
|
-
* check against the resolved name closes that loop.
|
|
45
|
-
*/
|
|
46
|
-
import { realpathSync } from 'node:fs';
|
|
47
|
-
import { basename, resolve, sep, relative } from 'node:path';
|
|
48
|
-
import { decidePermission } from '../permission.js';
|
|
49
|
-
import { resolveWorkspacePath } from '../path-security.js';
|
|
50
|
-
import { loadSettings } from '../settings.js';
|
|
51
|
-
/**
|
|
52
|
-
* Apply the full path-traversal + protected-file + symlink-escape gate
|
|
53
|
-
* for an inbound edit request. Returns the resolved absolute path on
|
|
54
|
-
* success; never throws on the security-relevant rejections. Filesystem
|
|
55
|
-
* errors that are NOT security-relevant (an unexpected EACCES on a
|
|
56
|
-
* realpath that doesn't involve a symlink, etc.) are re-thrown so the
|
|
57
|
-
* layer's existing error path records them as `write_error`.
|
|
58
|
-
*/
|
|
59
|
-
export function applySecurityGate(inputPath, opts) {
|
|
60
|
-
// (a) Workspace-scoped path resolution. `resolveWorkspacePath`
|
|
61
|
-
// throws on URL-encoded traversal, parent-chain escapes via `..`,
|
|
62
|
-
// and target-symlinks that point outside the workspace root. We
|
|
63
|
-
// funnel the throw into a structured rejection so the dispatcher
|
|
64
|
-
// can render a deterministic operator-facing message.
|
|
65
|
-
let resolved;
|
|
66
|
-
try {
|
|
67
|
-
resolved = resolveWorkspacePath(opts.cwd, inputPath);
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
-
return {
|
|
72
|
-
ok: false,
|
|
73
|
-
reason: 'path_outside_workspace',
|
|
74
|
-
detail: `${message}`,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
// (b) Protected-file gate. The same `decidePermission` call the
|
|
78
|
-
// `edit` / `write` tools make. We pass the workspace-relative form
|
|
79
|
-
// because `protectedTargetReason` uses `basename(resolve(root, target))`
|
|
80
|
-
// internally — passing an absolute path that already lives under
|
|
81
|
-
// `root` works because Node's `resolve` is idempotent, but the
|
|
82
|
-
// workspace-relative form keeps the audit log compact.
|
|
83
|
-
const settings = opts.settings ?? loadSettings(opts.cwd);
|
|
84
|
-
const targetForPermission = isInsideRoot(resolved, opts.cwd)
|
|
85
|
-
? relative(opts.cwd, resolved) || inputPath
|
|
86
|
-
: inputPath;
|
|
87
|
-
const decision = decidePermission({ tool: opts.toolName, kind: 'edit', target: targetForPermission }, settings, opts.cwd);
|
|
88
|
-
if (decision.decision !== 'allow') {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
reason: 'protected_file',
|
|
92
|
-
detail: `${decision.decision} for ${targetForPermission}: ${decision.reason}`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// (c) Symlink-escape re-check. Closes the TOCTOU window between
|
|
96
|
-
// `resolveWorkspacePath` and the apply-time write. If the target
|
|
97
|
-
// file does not yet exist (legitimate for any future "create"
|
|
98
|
-
// path), we walk up to the parent — realpathSync throws ENOENT on
|
|
99
|
-
// a missing leaf — and require the parent realpath to live inside
|
|
100
|
-
// the workspace realpath. The target's basename is then anchored to
|
|
101
|
-
// the parent realpath so a final-segment symlink swap cannot escape.
|
|
102
|
-
let realRoot;
|
|
103
|
-
try {
|
|
104
|
-
realRoot = realpathSync.native(opts.cwd);
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
-
return {
|
|
109
|
-
ok: false,
|
|
110
|
-
reason: 'symlink_escape',
|
|
111
|
-
detail: `cannot realpath workspace root: ${message}`,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
let realTarget;
|
|
115
|
-
try {
|
|
116
|
-
realTarget = realpathSync.native(resolved);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
const code = error.code;
|
|
120
|
-
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
121
|
-
// Unexpected filesystem error — re-throw so the layer records
|
|
122
|
-
// it as `write_error` rather than silently rejecting as
|
|
123
|
-
// `symlink_escape` (which would mis-attribute the cause to the
|
|
124
|
-
// operator).
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
// Target does not exist — anchor against the parent's realpath.
|
|
128
|
-
let realParent;
|
|
129
|
-
try {
|
|
130
|
-
realParent = realpathSync.native(resolve(resolved, '..'));
|
|
131
|
-
}
|
|
132
|
-
catch (parentError) {
|
|
133
|
-
const parentCode = parentError.code;
|
|
134
|
-
if (parentCode !== 'ENOENT' && parentCode !== 'ENOTDIR')
|
|
135
|
-
throw parentError;
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
reason: 'symlink_escape',
|
|
139
|
-
detail: `parent directory does not exist for ${inputPath}`,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (!isInsideRoot(realParent, realRoot)) {
|
|
143
|
-
return {
|
|
144
|
-
ok: false,
|
|
145
|
-
reason: 'symlink_escape',
|
|
146
|
-
detail: `parent realpath ${realParent} escapes workspace ${realRoot}`,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
// (d) Re-run the protected-file check against the parent's
|
|
150
|
-
// realpath-anchored basename. The basename of `resolved` is the
|
|
151
|
-
// final path segment the write will land on; pairing it with the
|
|
152
|
-
// resolved parent gives the workspace-relative form the
|
|
153
|
-
// protected-basename matcher needs. Closes the "create-new-file
|
|
154
|
-
// symlink-parent" variant: a dir symlink whose realpath stays
|
|
155
|
-
// inside the workspace must not let an attacker create a new
|
|
156
|
-
// `.env` (or any other protected basename) via a non-protected
|
|
157
|
-
// relative path.
|
|
158
|
-
const realCreateTarget = resolve(realParent, basename(resolved));
|
|
159
|
-
const recheck = recheckProtectedOnReal(realCreateTarget, realRoot, opts);
|
|
160
|
-
if (recheck)
|
|
161
|
-
return recheck;
|
|
162
|
-
return { ok: true, absPath: resolved };
|
|
163
|
-
}
|
|
164
|
-
if (!isInsideRoot(realTarget, realRoot)) {
|
|
165
|
-
return {
|
|
166
|
-
ok: false,
|
|
167
|
-
reason: 'symlink_escape',
|
|
168
|
-
detail: `target realpath ${realTarget} escapes workspace ${realRoot}`,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
// (d) Re-run the protected-file check on the resolved target. The
|
|
172
|
-
// pre-fix gate only checked the operator-supplied path; a symlink
|
|
173
|
-
// `alias -> .env` would slip through because `alias` is not
|
|
174
|
-
// protected. Now we re-key the permission decision on the realpath's
|
|
175
|
-
// workspace-relative form so the basename matcher sees `.env` (or
|
|
176
|
-
// any other protected suffix) the symlink points at. The settings
|
|
177
|
-
// pull is free — it is the same `settings` we loaded at step (b).
|
|
178
|
-
const recheck = recheckProtectedOnReal(realTarget, realRoot, opts);
|
|
179
|
-
if (recheck)
|
|
180
|
-
return recheck;
|
|
181
|
-
return { ok: true, absPath: resolved };
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Helper for step (d) of the gate. Re-runs `decidePermission` on the
|
|
185
|
-
* real (symlink-resolved) target's workspace-relative form and returns
|
|
186
|
-
* a `protected_file` rejection when the basename matches a protected
|
|
187
|
-
* pattern. Returns null when the recheck passes; the caller continues
|
|
188
|
-
* to `ok: true`.
|
|
189
|
-
*
|
|
190
|
-
* Lifted into a helper because both the existing-target branch and the
|
|
191
|
-
* missing-target (create) branch in `applySecurityGate` need the same
|
|
192
|
-
* check, and inlining it twice would invite drift the next time the
|
|
193
|
-
* protected-file rules grow.
|
|
194
|
-
*/
|
|
195
|
-
function recheckProtectedOnReal(realTarget, realRoot, opts) {
|
|
196
|
-
if (!isInsideRoot(realTarget, realRoot))
|
|
197
|
-
return null;
|
|
198
|
-
const realRelative = relative(realRoot, realTarget);
|
|
199
|
-
if (realRelative === '')
|
|
200
|
-
return null;
|
|
201
|
-
const settings = opts.settings ?? loadSettings(opts.cwd);
|
|
202
|
-
const realDecision = decidePermission({ tool: opts.toolName, kind: 'edit', target: realRelative }, settings, realRoot);
|
|
203
|
-
if (realDecision.decision !== 'allow') {
|
|
204
|
-
return {
|
|
205
|
-
ok: false,
|
|
206
|
-
reason: 'protected_file',
|
|
207
|
-
detail: `${realDecision.decision} for symlink target ${realRelative}: ${realDecision.reason}`,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* True iff `child` is `root` or strictly nested inside it. Uses the
|
|
214
|
-
* platform separator + a trailing `sep` guard to avoid the
|
|
215
|
-
* `/workspace` vs `/workspace-evil` false-positive that a naive
|
|
216
|
-
* `startsWith` check would produce.
|
|
217
|
-
*/
|
|
218
|
-
function isInsideRoot(child, root) {
|
|
219
|
-
if (child === root)
|
|
220
|
-
return true;
|
|
221
|
-
return child.startsWith(root + sep);
|
|
222
|
-
}
|
|
223
|
-
//# sourceMappingURL=security-gate.js.map
|