@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +2 -2
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +12 -12
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +140 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +286 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4151 -489
- package/dist/runtime/commands/agents.js +30 -30
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +32 -32
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +222 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +623 -45
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +124 -44
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +23 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output filter (primitive #2 of the 9-layer RAG architecture).
|
|
3
|
+
*
|
|
4
|
+
* Composes four independent safety checks into a single perimeter that
|
|
5
|
+
* runs against text the engine is ABOUT to emit to the operator. Each
|
|
6
|
+
* sub-check is a private function inside this module; the public entry
|
|
7
|
+
* point `filterOutput` runs them in a deterministic order and returns
|
|
8
|
+
* the union of their findings.
|
|
9
|
+
*
|
|
10
|
+
* The intent is "one chokepoint" — the engine wire-up (out of scope for
|
|
11
|
+
* this PR) will call `filterOutput` once per assistant turn and refuse
|
|
12
|
+
* to surface text whose `allowed` flag is false. Operators get a single
|
|
13
|
+
* structured violations list to triage.
|
|
14
|
+
*
|
|
15
|
+
* # Composed checks (deterministic order)
|
|
16
|
+
*
|
|
17
|
+
* 1. citation-verify — `[file:line]` / `(file:line)` patterns must
|
|
18
|
+
* resolve to a real file inside `workspaceRoot`.
|
|
19
|
+
* Path containment uses the existing workspace
|
|
20
|
+
* boundary check pattern from `path-security.ts`.
|
|
21
|
+
* 2. secret-scan — defers to `core/memory/secret-scanner.ts`
|
|
22
|
+
* (backlog). When `redactSecrets=true`
|
|
23
|
+
* (default) the filter substitutes detected
|
|
24
|
+
* secrets with `[SECRET:<kind>]` placeholders
|
|
25
|
+
* AND reports the violations.
|
|
26
|
+
* 3. numeric-claim — standalone numbers (≥3 digits OR percentages
|
|
27
|
+
* OR currency) must have a citation within
|
|
28
|
+
* 80 chars. When `allowNumericClaims=true` the
|
|
29
|
+
* check is skipped entirely.
|
|
30
|
+
* 4. malicious-url — every URL found in the text must intersect
|
|
31
|
+
* `allowedURLs`. Empty allowlist + non-empty URL
|
|
32
|
+
* set = all URLs flagged.
|
|
33
|
+
* 5. injection-echo — surface obvious prompt-injection fragments
|
|
34
|
+
* that look like upstream content leaked into
|
|
35
|
+
* the model's output (`<system-reminder>`,
|
|
36
|
+
* ChatML tags, "ignore previous instructions",
|
|
37
|
+
* etc.).
|
|
38
|
+
*
|
|
39
|
+
* # No early exit
|
|
40
|
+
*
|
|
41
|
+
* The filter walks every check on every call and accumulates every
|
|
42
|
+
* violation. A text with three secrets, two bad URLs and one bogus
|
|
43
|
+
* citation surfaces six violations — operators see the whole picture in
|
|
44
|
+
* one pass, not piecemeal across retries.
|
|
45
|
+
*
|
|
46
|
+
* # What this is NOT
|
|
47
|
+
*
|
|
48
|
+
* - A blocking guard. The caller decides what to do with
|
|
49
|
+
* `allowed=false`. We never throw on a violation — throwing would
|
|
50
|
+
* deny the caller the redacted text + structured findings.
|
|
51
|
+
* - A locale-aware numeric extractor. Only English thousands
|
|
52
|
+
* separators are recognised for now (`1,000`, `1000`, `1,000.50`).
|
|
53
|
+
* Locale variants (e.g. `1.000,50` in DE) are intentionally out of
|
|
54
|
+
* scope per the spec.
|
|
55
|
+
* - An ML-based harm classifier. Regex tier only; harm-pattern
|
|
56
|
+
* expansion is deferred.
|
|
57
|
+
*
|
|
58
|
+
* # Existing modules composed against
|
|
59
|
+
*
|
|
60
|
+
* - `core/memory/secret-scanner.ts` — secret-scan delegate.
|
|
61
|
+
* - `core/path-security.ts` — workspace boundary check
|
|
62
|
+
* pattern (containment +
|
|
63
|
+
* realpath).
|
|
64
|
+
*
|
|
65
|
+
* # Follow-ups (tracked, NOT shipped here)
|
|
66
|
+
*
|
|
67
|
+
* - Engine wire-up — consumer pulls `filterOutput` before emitting
|
|
68
|
+
* to the operator. Out of scope; touches `core/engine/` only.
|
|
69
|
+
* - Shared secret-scanner consolidation — depends on ship.
|
|
70
|
+
* - Locale-aware numeric extraction.
|
|
71
|
+
* - LLM-driven harm classifier (regex tier only today).
|
|
72
|
+
*/
|
|
73
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
74
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
75
|
+
import { redactSecrets as redactSecretsImpl, scanForSecrets, } from '../memory/secret-scanner.js';
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Pattern catalog
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Inline citation patterns. Accepts:
|
|
81
|
+
* - `[file:line]` e.g. `[src/foo.ts:42]`
|
|
82
|
+
* - `(file:line)` e.g. `(src/foo.ts:42)`
|
|
83
|
+
*
|
|
84
|
+
* The path must contain a `.` (file extension hint) to keep prose like
|
|
85
|
+
* `[chapter:1]` from being misread as a code citation. Line is a
|
|
86
|
+
* 1+ digit positive integer.
|
|
87
|
+
*
|
|
88
|
+
* Anchored with bounded character classes — no `.*` and no nested
|
|
89
|
+
* alternations, defending against catastrophic backtracking.
|
|
90
|
+
*/
|
|
91
|
+
const INLINE_CITATION_RE = /[\[(]([^\]\s)]+\.[A-Za-z0-9]+):(\d+)[\])]/g;
|
|
92
|
+
/**
|
|
93
|
+
* URL extraction. Matches `http://` or `https://` followed by host +
|
|
94
|
+
* optional path. Bounded path char class.
|
|
95
|
+
*/
|
|
96
|
+
const URL_RE = /\bhttps?:\/\/[A-Za-z0-9.\-]+(?::\d+)?(?:\/[A-Za-z0-9._~:/?#@!$&'()*+,;=%\-]*)?/g;
|
|
97
|
+
/**
|
|
98
|
+
* Numeric-claim patterns. We accumulate matches from THREE shapes:
|
|
99
|
+
* - percentage: `42%`, `0.5%`, `100.0%`
|
|
100
|
+
* - currency: `$1`, `$1,000`, `$1.50`, `€42`, `£99.99` (any 1+)
|
|
101
|
+
* - bare number: `123`, `1,000`, `42.5` (≥3 significant digits)
|
|
102
|
+
*
|
|
103
|
+
* "Significant digits" for the bare-number rule means ≥3 digit chars
|
|
104
|
+
* total ignoring separators. `99` (2 digits) is NOT flagged; `100` is.
|
|
105
|
+
* This dodges the false-positive trap of years / common small numbers
|
|
106
|
+
* appearing in prose without a citation needing to back them up.
|
|
107
|
+
*/
|
|
108
|
+
const PERCENT_RE = /\b\d+(?:\.\d+)?%/g;
|
|
109
|
+
const CURRENCY_RE = /[$€£¥₹]\s?\d{1,3}(?:,\d{3})*(?:\.\d+)?/g;
|
|
110
|
+
const BARE_NUMBER_RE = /\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b|\b\d{3,}(?:\.\d+)?\b/g;
|
|
111
|
+
/**
|
|
112
|
+
* Injection-echo fragments. These are markers we never expect a model
|
|
113
|
+
* to legitimately produce in its output; their appearance suggests
|
|
114
|
+
* upstream context (a user-supplied document, a tool result, a leaked
|
|
115
|
+
* system prompt) has bled into the generation surface.
|
|
116
|
+
*
|
|
117
|
+
* Kept literal-prefix-anchored to keep false-positives low. A model
|
|
118
|
+
* legitimately discussing prompt-injection education ("attackers might
|
|
119
|
+
* say `ignore previous instructions`") will trip the warning — the
|
|
120
|
+
* caller can downgrade via `allowed` post-hoc, but the violation must
|
|
121
|
+
* still surface so reviewers see it.
|
|
122
|
+
*/
|
|
123
|
+
const INJECTION_PATTERNS = [
|
|
124
|
+
{ id: 'system-reminder-tag', re: /<system-reminder\b/gi },
|
|
125
|
+
{ id: 'system-tag', re: /<\|im_start\|>/g },
|
|
126
|
+
{ id: 'endoftext-tag', re: /<\|endoftext\|>/g },
|
|
127
|
+
{ id: 'inst-marker', re: /\[INST\]/g },
|
|
128
|
+
{ id: 'inst-close', re: /\[\/INST\]/g },
|
|
129
|
+
{ id: 'ignore-previous', re: /\b(?:important[^a-z]*)?ignore\s+(?:all\s+|the\s+)?previous\s+(?:instructions|prompts?)/gi },
|
|
130
|
+
{ id: 'disregard-above', re: /\bdisregard\s+(?:all|prior|the)?\s*above/gi },
|
|
131
|
+
{ id: 'anthropic-human-tag', re: /\n\s*Human:\s*/g },
|
|
132
|
+
{ id: 'anthropic-assistant-tag', re: /\n\s*Assistant:\s*/g },
|
|
133
|
+
];
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Public entry point
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
export function filterOutput(text, options = {}) {
|
|
138
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
139
|
+
return { allowed: true, violations: [] };
|
|
140
|
+
}
|
|
141
|
+
const violations = [];
|
|
142
|
+
// Check order is deterministic — locked by the spec. Each check
|
|
143
|
+
// pushes its findings to `violations` and never throws.
|
|
144
|
+
// 1. Citation verify.
|
|
145
|
+
pushCitationViolations(text, options, violations);
|
|
146
|
+
// 2. Secret scan. We always scan; redaction is gated by the flag.
|
|
147
|
+
const redact = options.redactSecrets !== false;
|
|
148
|
+
const secretMatches = scanForSecrets(text);
|
|
149
|
+
for (const match of secretMatches) {
|
|
150
|
+
violations.push({
|
|
151
|
+
kind: 'secret-leak',
|
|
152
|
+
location: lineColumnFor(text, match.offset),
|
|
153
|
+
detail: `Secret pattern '${match.pattern}' (${match.confidence}) detected`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
let redactedText;
|
|
157
|
+
if (redact && secretMatches.length > 0) {
|
|
158
|
+
const result = redactSecretsImpl(text);
|
|
159
|
+
redactedText = result.redacted;
|
|
160
|
+
}
|
|
161
|
+
// 3. Numeric claim guard. Skipped when allowNumericClaims=true.
|
|
162
|
+
if (options.allowNumericClaims !== true) {
|
|
163
|
+
pushNumericClaimViolations(text, violations);
|
|
164
|
+
}
|
|
165
|
+
// 4. Malicious URL.
|
|
166
|
+
pushUrlViolations(text, options.allowedURLs ?? [], violations);
|
|
167
|
+
// 5. Injection echo.
|
|
168
|
+
pushInjectionEchoViolations(text, violations);
|
|
169
|
+
return {
|
|
170
|
+
allowed: violations.length === 0,
|
|
171
|
+
redactedText,
|
|
172
|
+
violations,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function collectInlineCitations(text) {
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const m of text.matchAll(INLINE_CITATION_RE)) {
|
|
178
|
+
if (m.index === undefined)
|
|
179
|
+
continue;
|
|
180
|
+
const path = m[1];
|
|
181
|
+
const lineStr = m[2];
|
|
182
|
+
if (path === undefined || lineStr === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
const lineNum = Number.parseInt(lineStr, 10);
|
|
185
|
+
if (!Number.isFinite(lineNum) || lineNum <= 0)
|
|
186
|
+
continue;
|
|
187
|
+
out.push({
|
|
188
|
+
raw: m[0],
|
|
189
|
+
path,
|
|
190
|
+
line: lineNum,
|
|
191
|
+
offset: m.index,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function pushCitationViolations(text, options, violations) {
|
|
197
|
+
const inline = collectInlineCitations(text);
|
|
198
|
+
const explicit = options.citations ?? [];
|
|
199
|
+
// Nothing to check.
|
|
200
|
+
if (inline.length === 0 && explicit.length === 0)
|
|
201
|
+
return;
|
|
202
|
+
const root = options.workspaceRoot;
|
|
203
|
+
if (root === undefined) {
|
|
204
|
+
// Fail-closed: cannot verify without a root, but we still surface
|
|
205
|
+
// the citations so the caller sees them.
|
|
206
|
+
for (const cite of inline) {
|
|
207
|
+
violations.push({
|
|
208
|
+
kind: 'invalid-citation',
|
|
209
|
+
location: lineColumnFor(text, cite.offset),
|
|
210
|
+
detail: `Citation '${cite.raw}' cannot be verified (workspaceRoot missing)`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
for (const cite of explicit) {
|
|
214
|
+
violations.push({
|
|
215
|
+
kind: 'invalid-citation',
|
|
216
|
+
detail: `Citation '${cite.path}:${cite.line}' cannot be verified (workspaceRoot missing)`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Canonicalise the workspace root once. realpath fail = pass-through;
|
|
222
|
+
// the containment check below will still reject any traversal.
|
|
223
|
+
let canonicalRoot;
|
|
224
|
+
try {
|
|
225
|
+
canonicalRoot = realpathSync.native(root);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
canonicalRoot = resolve(root);
|
|
229
|
+
}
|
|
230
|
+
for (const cite of inline) {
|
|
231
|
+
if (!isCitationContained(cite.path, canonicalRoot)) {
|
|
232
|
+
violations.push({
|
|
233
|
+
kind: 'invalid-citation',
|
|
234
|
+
location: lineColumnFor(text, cite.offset),
|
|
235
|
+
detail: `Citation '${cite.raw}' resolves outside workspace`,
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const abs = resolve(canonicalRoot, cite.path);
|
|
240
|
+
if (!existsSync(abs)) {
|
|
241
|
+
violations.push({
|
|
242
|
+
kind: 'invalid-citation',
|
|
243
|
+
location: lineColumnFor(text, cite.offset),
|
|
244
|
+
detail: `Citation '${cite.raw}' references missing file`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const cite of explicit) {
|
|
249
|
+
if (!isCitationContained(cite.path, canonicalRoot)) {
|
|
250
|
+
violations.push({
|
|
251
|
+
kind: 'invalid-citation',
|
|
252
|
+
detail: `Citation '${cite.path}:${cite.line}' resolves outside workspace`,
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const abs = resolve(canonicalRoot, cite.path);
|
|
257
|
+
if (!existsSync(abs)) {
|
|
258
|
+
violations.push({
|
|
259
|
+
kind: 'invalid-citation',
|
|
260
|
+
detail: `Citation '${cite.path}:${cite.line}' references missing file`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function isCitationContained(citationPath, canonicalRoot) {
|
|
266
|
+
if (isAbsolute(citationPath))
|
|
267
|
+
return false; // absolute paths in citations are never trusted
|
|
268
|
+
// Reject `..` segments before any FS work (mirror path-security.ts step #1).
|
|
269
|
+
const segments = citationPath.split(/[/\\]/);
|
|
270
|
+
if (segments.some((seg) => seg === '..'))
|
|
271
|
+
return false;
|
|
272
|
+
// Resolve relative to the canonical root and reverify containment.
|
|
273
|
+
const target = resolve(canonicalRoot, citationPath);
|
|
274
|
+
const rel = relative(canonicalRoot, target);
|
|
275
|
+
if (!rel)
|
|
276
|
+
return true;
|
|
277
|
+
return !rel.startsWith('..');
|
|
278
|
+
}
|
|
279
|
+
function collectNumericHits(text) {
|
|
280
|
+
const out = [];
|
|
281
|
+
for (const re of [PERCENT_RE, CURRENCY_RE, BARE_NUMBER_RE]) {
|
|
282
|
+
for (const m of text.matchAll(re)) {
|
|
283
|
+
if (m.index === undefined)
|
|
284
|
+
continue;
|
|
285
|
+
out.push({ value: m[0], offset: m.index });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Deduplicate overlapping hits — `100%` matches PERCENT_RE and the
|
|
289
|
+
// bare-number rule would not (anchored to no trailing `%`), but a
|
|
290
|
+
// currency `$100` could overlap a bare `100`. Sort by offset, keep
|
|
291
|
+
// the longest match at each anchor.
|
|
292
|
+
out.sort((a, b) => a.offset - b.offset || b.value.length - a.value.length);
|
|
293
|
+
const deduped = [];
|
|
294
|
+
let lastEnd = -1;
|
|
295
|
+
for (const hit of out) {
|
|
296
|
+
if (hit.offset < lastEnd)
|
|
297
|
+
continue;
|
|
298
|
+
deduped.push(hit);
|
|
299
|
+
lastEnd = hit.offset + hit.value.length;
|
|
300
|
+
}
|
|
301
|
+
return deduped;
|
|
302
|
+
}
|
|
303
|
+
function pushNumericClaimViolations(text, violations) {
|
|
304
|
+
const numbers = collectNumericHits(text);
|
|
305
|
+
if (numbers.length === 0)
|
|
306
|
+
return;
|
|
307
|
+
// Compute citation offsets ONCE — every inline citation in the text
|
|
308
|
+
// counts as anchorage for nearby numbers regardless of validity.
|
|
309
|
+
// (Validity is checked separately in pushCitationViolations.)
|
|
310
|
+
const citationOffsets = [];
|
|
311
|
+
for (const m of text.matchAll(INLINE_CITATION_RE)) {
|
|
312
|
+
if (m.index !== undefined)
|
|
313
|
+
citationOffsets.push(m.index);
|
|
314
|
+
}
|
|
315
|
+
for (const hit of numbers) {
|
|
316
|
+
if (!hasCitationWithin(hit.offset, hit.value.length, citationOffsets, 80)) {
|
|
317
|
+
violations.push({
|
|
318
|
+
kind: 'numeric-without-citation',
|
|
319
|
+
location: lineColumnFor(text, hit.offset),
|
|
320
|
+
detail: `Numeric claim '${hit.value}' has no citation within 80 chars`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function hasCitationWithin(hitOffset, hitLength, citationOffsets, window) {
|
|
326
|
+
const lo = hitOffset - window;
|
|
327
|
+
const hi = hitOffset + hitLength + window;
|
|
328
|
+
for (const cOff of citationOffsets) {
|
|
329
|
+
if (cOff >= lo && cOff <= hi)
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Malicious URL
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
function pushUrlViolations(text, allowed, violations) {
|
|
338
|
+
const normalisedAllowed = allowed.map((u) => stripTrailingSlash(u.toLowerCase()));
|
|
339
|
+
for (const m of text.matchAll(URL_RE)) {
|
|
340
|
+
if (m.index === undefined)
|
|
341
|
+
continue;
|
|
342
|
+
const url = m[0];
|
|
343
|
+
if (!isUrlAllowed(url, normalisedAllowed)) {
|
|
344
|
+
violations.push({
|
|
345
|
+
kind: 'malicious-url',
|
|
346
|
+
location: lineColumnFor(text, m.index),
|
|
347
|
+
detail: `URL '${url}' is not in the allowlist`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function stripTrailingSlash(s) {
|
|
353
|
+
return s.endsWith('/') ? s.slice(0, -1) : s;
|
|
354
|
+
}
|
|
355
|
+
function isUrlAllowed(url, allowlist) {
|
|
356
|
+
const lower = stripTrailingSlash(url.toLowerCase());
|
|
357
|
+
for (const allowed of allowlist) {
|
|
358
|
+
if (lower === allowed)
|
|
359
|
+
return true;
|
|
360
|
+
// Prefix-with-`/` match prevents `https://example.com.evil` from
|
|
361
|
+
// satisfying an `https://example.com` entry. We also accept the
|
|
362
|
+
// bare host without path.
|
|
363
|
+
if (lower.startsWith(`${allowed}/`))
|
|
364
|
+
return true;
|
|
365
|
+
if (lower.startsWith(`${allowed}?`))
|
|
366
|
+
return true;
|
|
367
|
+
if (lower.startsWith(`${allowed}#`))
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Injection echo
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
function pushInjectionEchoViolations(text, violations) {
|
|
376
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
377
|
+
// Clone each regex so concurrent calls do not share `lastIndex`.
|
|
378
|
+
const rx = new RegExp(pattern.re.source, pattern.re.flags);
|
|
379
|
+
for (const m of text.matchAll(rx)) {
|
|
380
|
+
if (m.index === undefined)
|
|
381
|
+
continue;
|
|
382
|
+
violations.push({
|
|
383
|
+
kind: 'injection-echo',
|
|
384
|
+
location: lineColumnFor(text, m.index),
|
|
385
|
+
detail: `Injection-echo pattern '${pattern.id}' matched (${truncateMatch(m[0])})`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function truncateMatch(s) {
|
|
391
|
+
if (s.length <= 64)
|
|
392
|
+
return s;
|
|
393
|
+
return `${s.slice(0, 64)}...`;
|
|
394
|
+
}
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Helpers
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
/**
|
|
399
|
+
* Compute 1-based line + column for a 0-based byte (UTF-16 code unit)
|
|
400
|
+
* offset within `text`. Mirrors the helper in `secret-scanner.ts` but
|
|
401
|
+
* returns column too — the spec demands a `{ line, column }` shape.
|
|
402
|
+
*/
|
|
403
|
+
function lineColumnFor(text, offset) {
|
|
404
|
+
let line = 1;
|
|
405
|
+
let column = 1;
|
|
406
|
+
const cap = Math.min(offset, text.length);
|
|
407
|
+
for (let i = 0; i < cap; i += 1) {
|
|
408
|
+
if (text.charCodeAt(i) === 10) {
|
|
409
|
+
line += 1;
|
|
410
|
+
column = 1;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
column += 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { line, column };
|
|
417
|
+
}
|
|
418
|
+
//# sourceMappingURL=output-filter.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
const PUGI_PREFIX = 'PUGI_';
|
|
4
|
+
function isPugiKey(key) {
|
|
5
|
+
return key.startsWith(PUGI_PREFIX);
|
|
6
|
+
}
|
|
7
|
+
function filterPugiVars(env) {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const [k, v] of Object.entries(env)) {
|
|
10
|
+
if (isPugiKey(k) && typeof v === 'string')
|
|
11
|
+
out[k] = v;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function isValidState(value) {
|
|
16
|
+
if (!value || typeof value !== 'object')
|
|
17
|
+
return false;
|
|
18
|
+
const v = value;
|
|
19
|
+
if (typeof v.cwd !== 'string')
|
|
20
|
+
return false;
|
|
21
|
+
if (!v.environment || typeof v.environment !== 'object' || Array.isArray(v.environment))
|
|
22
|
+
return false;
|
|
23
|
+
if (typeof v.updatedAt !== 'string')
|
|
24
|
+
return false;
|
|
25
|
+
if (v.personaSlug !== undefined && typeof v.personaSlug !== 'string')
|
|
26
|
+
return false;
|
|
27
|
+
for (const [k, entry] of Object.entries(v.environment)) {
|
|
28
|
+
if (typeof k !== 'string' || typeof entry !== 'string')
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function warn(message) {
|
|
34
|
+
process.stderr.write(`[pugi:env-file] ${message}\n`);
|
|
35
|
+
}
|
|
36
|
+
export async function loadEnvState(opts) {
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = await fs.readFile(opts.envFilePath, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const code = err.code;
|
|
43
|
+
if (code === 'ENOENT')
|
|
44
|
+
return null;
|
|
45
|
+
warn(`failed to read ${opts.envFilePath}: ${err.message}`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
warn(`malformed JSON at ${opts.envFilePath}: ${err.message}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (!isValidState(parsed)) {
|
|
57
|
+
warn(`invalid env state shape at ${opts.envFilePath}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const state = {
|
|
61
|
+
cwd: parsed.cwd,
|
|
62
|
+
environment: filterPugiVars(parsed.environment),
|
|
63
|
+
updatedAt: parsed.updatedAt,
|
|
64
|
+
};
|
|
65
|
+
if (parsed.personaSlug !== undefined)
|
|
66
|
+
state.personaSlug = parsed.personaSlug;
|
|
67
|
+
return state;
|
|
68
|
+
}
|
|
69
|
+
export async function saveEnvState(state, opts) {
|
|
70
|
+
const dir = dirname(opts.envFilePath);
|
|
71
|
+
await fs.mkdir(dir, { recursive: true });
|
|
72
|
+
const finalState = {
|
|
73
|
+
cwd: state.cwd,
|
|
74
|
+
environment: filterPugiVars(state.environment ?? {}),
|
|
75
|
+
updatedAt: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
if (state.personaSlug !== undefined)
|
|
78
|
+
finalState.personaSlug = state.personaSlug;
|
|
79
|
+
const payload = JSON.stringify(finalState, null, 2);
|
|
80
|
+
const tmpSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
81
|
+
const tmpPath = `${opts.envFilePath}.tmp-${tmpSuffix}`;
|
|
82
|
+
await fs.writeFile(tmpPath, payload, { encoding: 'utf8', mode: 0o600 });
|
|
83
|
+
try {
|
|
84
|
+
await fs.rename(tmpPath, opts.envFilePath);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
await fs.rm(tmpPath, { force: true }).catch(() => { });
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function mergeIntoEnv(state, processEnv) {
|
|
92
|
+
const out = {};
|
|
93
|
+
if (state) {
|
|
94
|
+
for (const [k, v] of Object.entries(state.environment)) {
|
|
95
|
+
if (isPugiKey(k) && typeof v === 'string')
|
|
96
|
+
out[k] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [k, v] of Object.entries(processEnv)) {
|
|
100
|
+
if (v !== undefined)
|
|
101
|
+
out[k] = v;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=env-file.js.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { estimateTokens } from '../compact/token-counter.js';
|
|
2
|
+
export const TRUNCATION_MARKER = '\n[…truncated…]\n';
|
|
3
|
+
const DEFAULT_PRIORITY = 0.5;
|
|
4
|
+
const SHAVE_STEP_RATIO = 0.1;
|
|
5
|
+
const SHAVE_SAFETY_BOUND = 10_000;
|
|
6
|
+
function validateInputs(sections) {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
for (const s of sections) {
|
|
9
|
+
if (typeof s.name !== 'string' || s.name.length === 0) {
|
|
10
|
+
throw new TypeError('section name must be a non-empty string');
|
|
11
|
+
}
|
|
12
|
+
if (seen.has(s.name)) {
|
|
13
|
+
throw new TypeError(`duplicate section name: ${s.name}`);
|
|
14
|
+
}
|
|
15
|
+
seen.add(s.name);
|
|
16
|
+
if (!Number.isFinite(s.budget) || s.budget < 0) {
|
|
17
|
+
throw new RangeError(`section ${s.name}: budget must be a finite number >= 0`);
|
|
18
|
+
}
|
|
19
|
+
if (s.priority !== undefined) {
|
|
20
|
+
if (!Number.isFinite(s.priority) || s.priority < 0 || s.priority > 1) {
|
|
21
|
+
throw new RangeError(`section ${s.name}: priority must be in [0, 1]`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function buildTruncated(content, keepChars, strategy) {
|
|
27
|
+
if (keepChars <= 0)
|
|
28
|
+
return '';
|
|
29
|
+
if (strategy === 'head') {
|
|
30
|
+
const start = Math.max(0, content.length - keepChars);
|
|
31
|
+
return TRUNCATION_MARKER + content.slice(start);
|
|
32
|
+
}
|
|
33
|
+
if (strategy === 'tail') {
|
|
34
|
+
return content.slice(0, keepChars) + TRUNCATION_MARKER;
|
|
35
|
+
}
|
|
36
|
+
const headChars = Math.floor(keepChars / 2);
|
|
37
|
+
const tailChars = keepChars - headChars;
|
|
38
|
+
const head = content.slice(0, headChars);
|
|
39
|
+
const tailStart = Math.max(headChars, content.length - tailChars);
|
|
40
|
+
const tail = content.slice(tailStart);
|
|
41
|
+
return head + TRUNCATION_MARKER + tail;
|
|
42
|
+
}
|
|
43
|
+
function truncateSection(name, content, budget, strategy) {
|
|
44
|
+
const originalTokens = estimateTokens(content);
|
|
45
|
+
if (originalTokens <= budget) {
|
|
46
|
+
return { content, tokens: originalTokens, truncated: false, droppedTokens: 0 };
|
|
47
|
+
}
|
|
48
|
+
if (strategy === 'none') {
|
|
49
|
+
throw new RangeError(`section ${name}: content (${originalTokens} tokens) exceeds budget (${budget}) and strategy='none'`);
|
|
50
|
+
}
|
|
51
|
+
if (budget === 0) {
|
|
52
|
+
return { content: '', tokens: 0, truncated: true, droppedTokens: originalTokens };
|
|
53
|
+
}
|
|
54
|
+
const markerTokens = estimateTokens(TRUNCATION_MARKER);
|
|
55
|
+
if (markerTokens >= budget) {
|
|
56
|
+
return { content: '', tokens: 0, truncated: true, droppedTokens: originalTokens };
|
|
57
|
+
}
|
|
58
|
+
const available = budget - markerTokens;
|
|
59
|
+
let keepChars = Math.max(0, available * 4);
|
|
60
|
+
let next = buildTruncated(content, keepChars, strategy);
|
|
61
|
+
let tokens = estimateTokens(next);
|
|
62
|
+
while (tokens > budget && keepChars > 0) {
|
|
63
|
+
keepChars = Math.max(0, keepChars - 4);
|
|
64
|
+
if (keepChars === 0) {
|
|
65
|
+
next = '';
|
|
66
|
+
tokens = 0;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
next = buildTruncated(content, keepChars, strategy);
|
|
70
|
+
tokens = estimateTokens(next);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
content: next,
|
|
74
|
+
tokens,
|
|
75
|
+
truncated: true,
|
|
76
|
+
droppedTokens: originalTokens - tokens,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function applySectionBudgets(sections, options) {
|
|
80
|
+
validateInputs(sections);
|
|
81
|
+
const sumBudgets = sections.reduce((sum, s) => sum + s.budget, 0);
|
|
82
|
+
const totalBudget = options?.totalBudget ?? sumBudgets;
|
|
83
|
+
const working = sections.map((s) => {
|
|
84
|
+
const r = truncateSection(s.name, s.content, s.budget, s.strategy);
|
|
85
|
+
return {
|
|
86
|
+
input: s,
|
|
87
|
+
currentBudget: s.budget,
|
|
88
|
+
content: r.content,
|
|
89
|
+
tokens: r.tokens,
|
|
90
|
+
truncated: r.truncated,
|
|
91
|
+
droppedTokens: r.droppedTokens,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
let totalTokens = working.reduce((sum, w) => sum + w.tokens, 0);
|
|
95
|
+
if (totalTokens > totalBudget) {
|
|
96
|
+
const shrinkable = working
|
|
97
|
+
.map((w, idx) => ({ w, idx, priority: w.input.priority ?? DEFAULT_PRIORITY }))
|
|
98
|
+
.filter((x) => x.w.input.strategy !== 'none')
|
|
99
|
+
.sort((a, b) => a.priority - b.priority || a.idx - b.idx);
|
|
100
|
+
let safety = 0;
|
|
101
|
+
while (totalTokens > totalBudget && safety < SHAVE_SAFETY_BOUND) {
|
|
102
|
+
safety++;
|
|
103
|
+
let shaved = false;
|
|
104
|
+
for (const x of shrinkable) {
|
|
105
|
+
if (x.w.currentBudget === 0 && x.w.tokens === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const step = Math.max(1, Math.ceil(x.w.input.budget * SHAVE_STEP_RATIO));
|
|
108
|
+
const newBudget = Math.max(0, x.w.currentBudget - step);
|
|
109
|
+
if (newBudget === x.w.currentBudget)
|
|
110
|
+
continue;
|
|
111
|
+
x.w.currentBudget = newBudget;
|
|
112
|
+
const r = truncateSection(x.w.input.name, x.w.input.content, newBudget, x.w.input.strategy);
|
|
113
|
+
const delta = x.w.tokens - r.tokens;
|
|
114
|
+
x.w.content = r.content;
|
|
115
|
+
x.w.tokens = r.tokens;
|
|
116
|
+
x.w.truncated = r.truncated;
|
|
117
|
+
x.w.droppedTokens = r.droppedTokens;
|
|
118
|
+
totalTokens -= delta;
|
|
119
|
+
shaved = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (!shaved)
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const finalSections = working.map((w) => ({
|
|
127
|
+
name: w.input.name,
|
|
128
|
+
content: w.content,
|
|
129
|
+
tokens: w.tokens,
|
|
130
|
+
truncated: w.truncated,
|
|
131
|
+
droppedTokens: w.droppedTokens,
|
|
132
|
+
}));
|
|
133
|
+
return {
|
|
134
|
+
sections: finalSections,
|
|
135
|
+
totalTokens,
|
|
136
|
+
totalBudget,
|
|
137
|
+
overBudget: totalTokens > totalBudget,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=section-budgets.js.map
|