diff --git a/README.md b/README.md index dccb635..62f5a39 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,10 @@ Available commands: You: ^C ``` +## Architecture + +The agent uses the Vercel AI SDK's `streamText` API with `stopWhen: stepCountIs(n)` for step control. This replaces the older `maxSteps` / `continueUntil` pattern—`stepCountIs` provides a clearer, declarative way to limit tool-call round-trips (e.g., `stepCountIs(1)` allows one tool invocation cycle per stream call). The outer conversation loop in the CLI drives multi-turn interaction. + ## Model Uses `LGAI-EXAONE/K-EXAONE-236B-A23B` via FriendliAI serverless endpoints by default. Use `/model` command to switch models. diff --git a/bun.lock b/bun.lock index 0277135..d94d574 100644 --- a/bun.lock +++ b/bun.lock @@ -9,9 +9,9 @@ "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/openai-compatible": "^2.0.30", "@friendliai/ai-provider": "^1.1.4", + "@mariozechner/pi-tui": "^0.54.0", "@t3-oss/env-core": "^0.13.10", "ai": "^6.0.94", - "ai-sdk-provider-gemini-cli": "^2.0.1", "glob": "^13.0.6", "ignore": "^7.0.5", "yaml": "^2.8.2", @@ -43,10 +43,6 @@ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@biomejs/biome": ["@biomejs/biome@2.4.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.0", "@biomejs/cli-darwin-x64": "2.4.0", "@biomejs/cli-linux-arm64": "2.4.0", "@biomejs/cli-linux-arm64-musl": "2.4.0", "@biomejs/cli-linux-x64": "2.4.0", "@biomejs/cli-linux-x64-musl": "2.4.0", "@biomejs/cli-win32-arm64": "2.4.0", "@biomejs/cli-win32-x64": "2.4.0" }, "bin": { "biome": "bin/biome" } }, "sha512-iluT61cORUDIC5i/y42ljyQraCemmmcgbMLLCnYO+yh+2hjTmcMFcwY8G0zTzWCsPb3t3AyKc+0t/VuhPZULUg=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+YpOtPSuU0etomfvFTPWRsa7+8ejaJL3yaROEoT/96HDJbR6OsvZQk0C8JUYou+XFdP+JcGxqZknkp4n934RA=="], @@ -123,936 +119,96 @@ "@friendliai/ai-provider": ["@friendliai/ai-provider@1.1.4", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.30", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.12" } }, "sha512-9TU4B1QFqPhbkONjI5afCF7Ox4jOqtGg1xw8mA9QHZdtlEbZxU+mBNvMPlI5pU5kPoN6s7wkXmFmxpID+own1A=="], - "@google-cloud/common": ["@google-cloud/common@5.0.2", "", { "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" } }, "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA=="], - - "@google-cloud/logging": ["@google-cloud/logging@11.2.1", "", { "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" } }, "sha512-2h9HBJG3OAsvzXmb81qXmaTPfXYU7KJTQUxunoOKFGnY293YQ/eCkW1Y5mHLocwpEqeqQYT/Qvl6Tk+Q7PfStw=="], - - "@google-cloud/opentelemetry-cloud-monitoring-exporter": ["@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.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" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-metrics": "^2.0.0" } }, "sha512-+lAew44pWt6rA4l8dQ1gGhH7Uo95wZKfq/GBf9aEyuNDDLQ2XppGEEReu6ujesSqTtZ8ueQFt73+7SReSHbwqg=="], - - "@google-cloud/opentelemetry-cloud-trace-exporter": ["@google-cloud/opentelemetry-cloud-trace-exporter@3.0.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" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@opentelemetry/core": "^2.0.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0" } }, "sha512-mUfLJBFo+ESbO0dAGboErx2VyZ7rbrHcQvTP99yH/J72dGaPbH2IzS+04TFbTbEd1VW5R9uK3xq2CqawQaG+1Q=="], - - "@google-cloud/opentelemetry-resource-util": ["@google-cloud/opentelemetry-resource-util@3.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.22.0", "gcp-metadata": "^6.0.0" }, "peerDependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/resources": "^2.0.0" } }, "sha512-CGR/lNzIfTKlZoZFfS6CkVzx+nsC9gzy6S8VcyaLegfEJbiPjxbMLP7csyhJTvZe/iRRcQJxSk0q8gfrGqD3/Q=="], - - "@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="], - - "@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="], - - "@google-cloud/projectify": ["@google-cloud/projectify@4.0.0", "", {}, "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA=="], - - "@google-cloud/promisify": ["@google-cloud/promisify@4.0.0", "", {}, "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g=="], - - "@google/gemini-cli-core": ["@google/gemini-cli-core@0.22.4", "", { "dependencies": { "@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", "@iarna/toml": "^2.2.5", "@joshua.litt/get-ripgrep": "^0.0.3", "@modelcontextprotocol/sdk": "^1.23.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-logs-otlp-http": "^0.203.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.203.0", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/instrumentation-http": "^0.203.0", "@opentelemetry/resource-detector-gcp": "^0.40.0", "@opentelemetry/sdk-node": "^0.203.0", "@types/glob": "^8.1.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", "diff": "^7.0.0", "dotenv": "^17.1.0", "fast-levenshtein": "^2.0.6", "fast-uri": "^3.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", "marked": "^15.0.12", "mime": "4.0.7", "mnemonist": "^0.40.3", "open": "^10.1.2", "picomatch": "^4.0.1", "read-package-up": "^11.0.0", "shell-quote": "^1.8.3", "simple-git": "^3.28.0", "strip-ansi": "^7.1.0", "tree-sitter-bash": "^0.25.0", "undici": "^7.10.0", "web-tree-sitter": "^0.25.10", "ws": "^8.18.0", "zod": "^3.25.76" }, "optionalDependencies": { "@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" } }, "sha512-tJXajzxWXkSU8jVfwPG6rEFtUg9Bi3I+YAcTUzLEeaNITHJX+1IV0cVvi3/qguz6dWAnYM0mQ3U9jXvfyvIDPg=="], - - "@google/genai": ["@google/genai@1.30.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.20.1" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.8.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" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], - - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], - - "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], - - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "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" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@joshua.litt/get-ripgrep": ["@joshua.litt/get-ripgrep@0.0.3", "", { "dependencies": { "@lvce-editor/verror": "^1.6.0", "execa": "^9.5.2", "extract-zip": "^2.0.1", "fs-extra": "^11.3.0", "got": "^14.4.5", "path-exists": "^5.0.0", "xdg-basedir": "^5.1.0" } }, "sha512-rycdieAKKqXi2bsM7G2ayDiNk5CAX8ZOzsTQsirfOqUKPef04Xw40BWGGyimaOOuvPgLWYt3tPnLLG3TvPXi5Q=="], - - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - - "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], - - "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], - - "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], - - "@lvce-editor/verror": ["@lvce-editor/verror@1.7.0", "", {}, "sha512-+LGuAEIC2L7pbvkyAQVWM2Go0dAy+UWEui28g07zNtZsCBhm+gusBK8PNwLJLV5Jay+TyUYuwLIbJdjLLzqEBg=="], - - "@lydell/node-pty": ["@lydell/node-pty@1.1.0", "", { "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" } }, "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw=="], - - "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w=="], - - "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-XZdvqj5FjAMjH8bdp0YfaZjur5DrCIDD1VYiE9EkkYVMDQqRUPHYV3U8BVEQVT9hYfjmpr7dNaELF2KyISWSNA=="], - - "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg=="], - - "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-NcNqRTD14QT+vXcEuqSSvmWY+0+WUBn2uRE8EN0zKtDpIEr9d+YiFj16Uqds6QfcLCHfZmC+Ls7YzwTaqDnanA=="], - - "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-JOMbCou+0fA7d/m97faIIfIU0jOv8sn2OR7tI45u3AmldKoKoLP8zHY6SAvDDnI3fccO1R2HeR1doVjpS7HM0w=="], - - "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3N56BZ+WDFnUMYRtsrr7Ky2mhWGl9xXcyqR6cexfuCqcz9RNWL+KoXRv/nZylY5dYaXkft4JaR1uVu+roiZDAw=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "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.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "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.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], + "@mariozechner/pi-tui": ["@mariozechner/pi-tui@0.54.0", "", { "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", "koffi": "^2.9.0", "marked": "^15.0.12", "mime-types": "^3.0.1" } }, "sha512-bvFlUohdxDvKcFeQM2xsd5twCGKWxVaYSlHCFljIW0KqMC4vU+/Ts4A1i9iDnm6Xe/MlueKvC0V09YeC8fLIHA=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ=="], - - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw=="], - - "@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], - - "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.203.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/sdk-logs": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw=="], - - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/sdk-logs": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw=="], - - "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw=="], - - "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.203.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ=="], - - "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ=="], - - "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg=="], - - "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-metrics": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg=="], - - "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.203.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA=="], - - "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw=="], - - "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ=="], - - "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw=="], - - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ=="], - - "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g=="], - - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], - - "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.203.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.203.0", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ=="], - - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A=="], - - "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw=="], - - "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w=="], - - "@opentelemetry/resource-detector-gcp": ["@opentelemetry/resource-detector-gcp@0.40.3", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/resources": "^2.0.0", "gcp-metadata": "^6.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-C796YjBA5P1JQldovApYfFA/8bQwFfpxjUbOtGhn1YZkVTLoNQN+kvBwgALfTPWzug6fWsd0xhn9dzeiUcndag=="], - - "@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="], - - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw=="], - - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g=="], - - "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.203.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.203.0", "@opentelemetry/exporter-logs-otlp-http": "0.203.0", "@opentelemetry/exporter-logs-otlp-proto": "0.203.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.203.0", "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.203.0", "@opentelemetry/exporter-prometheus": "0.203.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.203.0", "@opentelemetry/exporter-trace-otlp-http": "0.203.0", "@opentelemetry/exporter-trace-otlp-proto": "0.203.0", "@opentelemetry/exporter-zipkin": "2.0.1", "@opentelemetry/instrumentation": "0.203.0", "@opentelemetry/propagator-b3": "2.0.1", "@opentelemetry/propagator-jaeger": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.203.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "@opentelemetry/sdk-trace-node": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ=="], - - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], - - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.0.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.0.1", "@opentelemetry/core": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA=="], - - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], - - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - - "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], - - "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], - - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], - "@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="], - - "@types/glob": ["@types/glob@8.1.0", "", { "dependencies": { "@types/minimatch": "^5.1.2", "@types/node": "*" } }, "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w=="], - - "@types/html-to-text": ["@types/html-to-text@9.0.4", "", {}, "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ=="], - - "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="], - - "@types/long": ["@types/long@4.0.2", "", {}, "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="], - - "@types/minimatch": ["@types/minimatch@5.1.2", "", {}, "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="], + "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], - "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - - "@types/request": ["@types/request@2.48.13", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.5" } }, "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg=="], - - "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], - - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], - "@xterm/headless": ["@xterm/headless@5.5.0", "", {}, "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g=="], - - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@6.0.94", "", { "dependencies": { "@ai-sdk/gateway": "3.0.52", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/F9wh262HbK05b/5vILh38JvPiheonT+kBj1L97712E7VPchqmcx7aJuZN3QSk5Pj6knxUJLm2FFpYJI1pHXUA=="], - "ai-sdk-provider-gemini-cli": ["ai-sdk-provider-gemini-cli@2.0.1", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@google/gemini-cli-core": "0.22.4", "@google/genai": "1.30.0", "google-auth-library": "^9.11.0", "zod-to-json-schema": "3.25.0" }, "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-v9Oc9irtWalFjODdj6nUFg0ifNJYm6IiWoafNdsJINmgE2k5JC0gEouypPsGoX9RAkIlOsJiE3ujbd+6nUqXxw=="], - - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - - "body-parser": ["body-parser@2.2.2", "", { "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" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - - "byte-counter": ["byte-counter@0.1.0", "", {}, "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], - - "cacheable-request": ["cacheable-request@13.0.18", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.4", "get-stream": "^9.0.1", "http-cache-semantics": "^4.2.0", "keyv": "^5.5.5", "mimic-response": "^4.0.0", "normalize-url": "^8.1.1", "responselike": "^4.0.2" } }, "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], - "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decompress-response": ["decompress-response@10.0.0", "", { "dependencies": { "mimic-response": "^4.0.0" } }, "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q=="], - "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], - - "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], - - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - - "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], - - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - - "eventid": ["eventid@2.0.1", "", { "dependencies": { "uuid": "^8.0.0" } }, "sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "execa": ["execa@9.6.1", "", { "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" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], - - "express": ["express@5.2.1", "", { "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" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "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" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "find-up-simple": ["find-up-simple@1.0.1", "", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - - "form-data": ["form-data@2.5.5", "", { "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" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], - - "form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], - - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], - - "gaxios": ["gaxios@6.7.1", "", { "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" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], - - "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "google-auth-library": ["google-auth-library@9.15.1", "", { "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" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], - - "google-gax": ["google-gax@4.6.1", "", { "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" } }, "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ=="], - - "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], - - "googleapis": ["googleapis@137.1.0", "", { "dependencies": { "google-auth-library": "^9.0.0", "googleapis-common": "^7.0.0" } }, "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ=="], - - "googleapis-common": ["googleapis-common@7.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" } }, "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "got": ["got@14.6.6", "", { "dependencies": { "@sindresorhus/is": "^7.0.1", "byte-counter": "^0.1.0", "cacheable-lookup": "^7.0.0", "cacheable-request": "^13.0.12", "decompress-response": "^10.0.0", "form-data-encoder": "^4.0.2", "http2-wrapper": "^2.2.1", "keyv": "^5.5.3", "lowercase-keys": "^3.0.0", "p-cancelable": "^4.0.1", "responselike": "^4.0.2", "type-fest": "^4.26.1" } }, "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], - - "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - - "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], - - "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], - - "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], - - "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], - - "http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], - - "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - - "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], - - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - - "keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], - - "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - - "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + "koffi": ["koffi@2.15.1", "", {}, "sha512-mnc0C0crx/xMSljb5s9QbnLrlFHprioFO1hkXyuSuO/QtbpLDa0l/uM21944UfQunMKmp3/r789DTDxVyyH6aA=="], "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime": ["mime@4.0.7", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], - "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mnemonist": ["mnemonist@0.40.3", "", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ=="], - - "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], - - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - - "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], - - "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], - - "normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], - - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "obliterator": ["obliterator@2.0.5", "", {}, "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - - "p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], - - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - - "proto3-json-serializer": ["proto3-json-serializer@2.0.2", "", { "dependencies": { "protobufjs": "^7.2.5" } }, "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ=="], - - "protobufjs": ["protobufjs@7.5.4", "", { "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" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], - - "pumpify": ["pumpify@2.0.1", "", { "dependencies": { "duplexify": "^4.1.1", "inherits": "^2.0.3", "pump": "^3.0.0" } }, "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw=="], - - "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], - - "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="], - - "read-pkg": ["read-pkg@9.0.1", "", { "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" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "responselike": ["responselike@4.0.2", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA=="], - - "retry-request": ["retry-request@7.0.2", "", { "dependencies": { "@types/request": "^2.48.8", "extend": "^3.0.2", "teeny-request": "^9.0.0" } }, "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w=="], - - "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "send": ["send@1.2.1", "", { "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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - - "side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-git": ["simple-git@3.30.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], - - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], - - "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - - "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="], - - "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - - "stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "teeny-request": ["teeny-request@9.0.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" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "tree-sitter-bash": ["tree-sitter-bash@0.25.1", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ultracite": ["ultracite@7.2.3", "", { "dependencies": { "@clack/prompts": "^1.0.1", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.3", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-WKNS2sKAZe4BHu+JGbZebXvy/A1QagDaBnndrK/zwOJAze/mQ8jeHfdG2bPlv3qcJ5fdS3w2Kd7c/eIcH78HvA=="], - "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - - "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "yargs": ["yargs@17.7.2", "", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], - - "@google/gemini-cli-core/glob": ["glob@12.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" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="], - - "@google/gemini-cli-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "@google/genai/google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - - "@types/glob/@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - - "@types/request/@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - - "@types/yauzl/@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - - "ai-sdk-provider-gemini-cli/@ai-sdk/provider": ["@ai-sdk/provider@3.0.4", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5KXyBOSEX+l67elrEa+wqo/LSsSTtrPj9Uoh3zMbe/ceQX4ucHI3b9nUEfNkGF3Ry1svv90widAt+aiKdIJasQ=="], - - "ai-sdk-provider-gemini-cli/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.8", "", { "dependencies": { "@ai-sdk/provider": "3.0.4", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ns9gN7MmpI8vTRandzgz+KK/zNMLzhrriiKECMt4euLtQFSBgNfydtagPOX4j4pS1/3KvHF6RivhT3gNQgBZsg=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "eventid/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - - "execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], - - "get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "google-gax/@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "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" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], - - "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "npm-run-path/unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - - "protobufjs/@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - - "rimraf/glob": ["glob@10.5.0", "", { "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" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - - "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@google/gemini-cli-core/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - - "@google/gemini-cli-core/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "@google/gemini-cli-core/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], - - "@google/genai/google-auth-library/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], - - "@google/genai/google-auth-library/gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], - - "@google/genai/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@types/glob/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@types/request/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@types/yauzl/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@google/genai/google-auth-library/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/package.json b/package.json index 7a093d8..e5085aa 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/openai-compatible": "^2.0.30", "@friendliai/ai-provider": "^1.1.4", + "@mariozechner/pi-tui": "^0.54.0", "@t3-oss/env-core": "^0.13.10", "ai": "^6.0.94", - "ai-sdk-provider-gemini-cli": "^2.0.1", "glob": "^13.0.6", "ignore": "^7.0.5", "yaml": "^2.8.2", diff --git a/src/agent.ts b/src/agent.ts index cfb0dfc..4075eb2 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1,8 +1,7 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createFriendli } from "@friendliai/ai-provider"; import type { ModelMessage } from "ai"; -import { ToolLoopAgent, wrapLanguageModel } from "ai"; -import { createGeminiProvider } from "ai-sdk-provider-gemini-cli"; +import { stepCountIs, streamText, wrapLanguageModel } from "ai"; import { getEnvironmentContext } from "./context/environment-context"; import { loadSkillsMetadata } from "./context/skills"; import { SYSTEM_PROMPT } from "./context/system-prompt"; @@ -12,41 +11,34 @@ import { buildTodoContinuationPrompt, getIncompleteTodos, } from "./middleware/todo-continuation"; +import { + DEFAULT_TOOL_FALLBACK_MODE, + LEGACY_ENABLED_TOOL_FALLBACK_MODE, + type ToolFallbackMode, +} from "./tool-fallback-mode"; import { tools } from "./tools"; export const DEFAULT_MODEL_ID = "MiniMaxAI/MiniMax-M2.5"; -export const DEFAULT_ANTHROPIC_MODEL_ID = "claude-sonnet-4-5-20250929"; -export const DEFAULT_GEMINI_MODEL_ID = "gemini-2.5-pro"; +export const DEFAULT_ANTHROPIC_MODEL_ID = "claude-sonnet-4-6"; const OUTPUT_TOKEN_MAX = 64_000; -export type ProviderType = "friendli" | "anthropic" | "gemini"; +type CoreStreamResult = ReturnType; -export const ANTHROPIC_MODELS = [ - { id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5 (Latest)" }, - { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5 (Latest)" }, -] as const; +export interface AgentStreamOptions { + abortSignal?: AbortSignal; +} + +export interface AgentStreamResult { + finishReason: CoreStreamResult["finishReason"]; + fullStream: CoreStreamResult["fullStream"]; + response: CoreStreamResult["response"]; +} + +export type ProviderType = "friendli" | "anthropic"; -export const GEMINI_MODELS = [ - { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - thinkingType: "thinkingLevel", - }, - { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - thinkingType: "thinkingLevel", - }, - { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - thinkingType: "thinkingBudget", - }, - { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - thinkingType: "thinkingBudget", - }, +export const ANTHROPIC_MODELS = [ + { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Latest)" }, + { id: "claude-opus-4-6", name: "Claude Opus 4.6 (Latest)" }, ] as const; const friendli = env.FRIENDLI_TOKEN @@ -62,35 +54,14 @@ const anthropic = env.ANTHROPIC_API_KEY }) : null; -const gemini = createGeminiProvider({ authType: "oauth-personal" }); - -type GeminiModel = (typeof GEMINI_MODELS)[number]; - -function getGeminiThinkingConfig( - model: GeminiModel | undefined, - enabled: boolean -) { - if (!enabled) { - return {}; - } - if (model?.thinkingType === "thinkingLevel") { - return { thinkingConfig: { thinkingLevel: "medium" as const } }; - } - return { thinkingConfig: { thinkingBudget: 10_000 } }; -} - interface CreateAgentOptions { enableThinking?: boolean; - enableToolFallback?: boolean; instructions?: string; provider?: ProviderType; + toolFallbackMode?: ToolFallbackMode; } -const getModel = ( - modelId: string, - provider: ProviderType, - thinkingEnabled = false -) => { +const getModel = (modelId: string, provider: ProviderType) => { if (provider === "anthropic") { if (!anthropic) { throw new Error( @@ -100,15 +71,6 @@ const getModel = ( return anthropic(modelId); } - if (provider === "gemini") { - const geminiModel = GEMINI_MODELS.find((m) => m.id === modelId); - const thinkingConfig = getGeminiThinkingConfig( - geminiModel, - thinkingEnabled - ); - return gemini(modelId, thinkingConfig); - } - if (!friendli) { throw new Error( "FRIENDLI_TOKEN is not set. Please set it in your environment." @@ -123,15 +85,13 @@ const ANTHROPIC_MAX_OUTPUT_TOKENS = 64_000; const createAgent = (modelId: string, options: CreateAgentOptions = {}) => { const provider = options.provider ?? "friendli"; const thinkingEnabled = options.enableThinking ?? false; - const model = getModel(modelId, provider, thinkingEnabled); + const model = getModel(modelId, provider); const getAnthropicProviderOptions = () => { if (!thinkingEnabled) { return undefined; } - // Opus 4.5: use effort parameter - // Sonnet 4.5: use thinking with budgetTokens const isOpus = modelId.includes("opus"); if (isOpus) { return { anthropic: { effort: "high" } }; @@ -150,10 +110,6 @@ const createAgent = (modelId: string, options: CreateAgentOptions = {}) => { if (provider === "anthropic") { return getAnthropicProviderOptions(); } - if (provider === "gemini") { - // Gemini thinking is configured in the model creation - return undefined; - } return { friendli: { chat_template_kwargs: { @@ -173,18 +129,33 @@ const createAgent = (modelId: string, options: CreateAgentOptions = {}) => { ? ANTHROPIC_MAX_OUTPUT_TOKENS - ANTHROPIC_THINKING_BUDGET_TOKENS : OUTPUT_TOKEN_MAX; - return new ToolLoopAgent({ - model: wrapLanguageModel({ - model, - middleware: buildMiddlewares({ - enableToolFallback: options.enableToolFallback ?? false, - }), + const wrappedModel = wrapLanguageModel({ + model, + middleware: buildMiddlewares({ + toolFallbackMode: options.toolFallbackMode ?? DEFAULT_TOOL_FALLBACK_MODE, }), - instructions: options.instructions || SYSTEM_PROMPT, - tools, - maxOutputTokens, - providerOptions, }); + + return { + stream: ({ + messages, + abortSignal, + }: { messages: ModelMessage[] } & AgentStreamOptions) => { + return streamText({ + model: wrappedModel, + system: options.instructions ?? SYSTEM_PROMPT, + tools, + messages, + maxOutputTokens, + providerOptions, + // stepCountIs(n) replaces the deprecated maxSteps option. + // It configures the stream to stop after n tool-call round-trips, + // giving the model a single tool invocation cycle before returning. + stopWhen: stepCountIs(1), + abortSignal, + }); + }, + }; }; export type ModelType = "serverless" | "dedicated"; @@ -195,7 +166,7 @@ class AgentManager { private provider: ProviderType = "friendli"; private headlessMode = false; private thinkingEnabled = false; - private toolFallbackEnabled = false; + private toolFallbackMode: ToolFallbackMode = DEFAULT_TOOL_FALLBACK_MODE; getModelId(): string { return this.modelId; @@ -221,8 +192,6 @@ class AgentManager { this.provider = provider; if (provider === "anthropic") { this.modelId = DEFAULT_ANTHROPIC_MODEL_ID; - } else if (provider === "gemini") { - this.modelId = DEFAULT_GEMINI_MODEL_ID; } else { this.modelId = DEFAULT_MODEL_ID; } @@ -244,12 +213,22 @@ class AgentManager { return this.thinkingEnabled; } + getToolFallbackMode(): ToolFallbackMode { + return this.toolFallbackMode; + } + + setToolFallbackMode(mode: ToolFallbackMode): void { + this.toolFallbackMode = mode; + } + setToolFallbackEnabled(enabled: boolean): void { - this.toolFallbackEnabled = enabled; + this.toolFallbackMode = enabled + ? LEGACY_ENABLED_TOOL_FALLBACK_MODE + : DEFAULT_TOOL_FALLBACK_MODE; } isToolFallbackEnabled(): boolean { - return this.toolFallbackEnabled; + return this.toolFallbackMode !== DEFAULT_TOOL_FALLBACK_MODE; } async getInstructions(): Promise { @@ -272,14 +251,17 @@ class AgentManager { return tools; } - async stream(messages: ModelMessage[]) { + async stream( + messages: ModelMessage[], + options: AgentStreamOptions = {} + ): Promise { const agent = createAgent(this.modelId, { instructions: await this.getInstructions(), enableThinking: this.thinkingEnabled, - enableToolFallback: this.toolFallbackEnabled, + toolFallbackMode: this.toolFallbackMode, provider: this.provider, }); - return agent.stream({ messages }); + return agent.stream({ messages, ...options }); } } diff --git a/src/commands/aliases-and-tool-fallback.test.ts b/src/commands/aliases-and-tool-fallback.test.ts new file mode 100644 index 0000000..b210eef --- /dev/null +++ b/src/commands/aliases-and-tool-fallback.test.ts @@ -0,0 +1,128 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { agentManager } from "../agent"; +import { + DEFAULT_TOOL_FALLBACK_MODE, + LEGACY_ENABLED_TOOL_FALLBACK_MODE, + TOOL_FALLBACK_MODES, + type ToolFallbackMode, +} from "../tool-fallback-mode"; +import { createHelpCommand } from "./help"; +import { executeCommand, registerCommand } from "./index"; +import { createToolFallbackCommand } from "./tool-fallback"; +import type { Command } from "./types"; + +let commandCounter = 0; + +const nextCommandName = (prefix: string): string => { + commandCounter += 1; + return `${prefix}-${commandCounter}`; +}; + +describe("Command aliases", () => { + it("executes canonical command via alias", async () => { + const canonicalName = nextCommandName("clear"); + const aliasName = nextCommandName("new"); + + registerCommand({ + name: canonicalName, + aliases: [aliasName], + description: "Alias command test", + execute: () => ({ + success: true, + message: `resolved:${canonicalName}`, + }), + }); + + const result = await executeCommand(`/${aliasName}`); + + expect(result).not.toBeNull(); + expect(result?.success).toBe(true); + expect(result?.message).toBe(`resolved:${canonicalName}`); + }); + + it("renders merged display name in help output", async () => { + const commandMap = new Map(); + commandMap.set("clear", { + name: "clear", + displayName: "clear (new)", + aliases: ["new"], + description: "Start a new session", + execute: () => ({ success: true, action: "new-session" }), + }); + + const help = createHelpCommand(() => commandMap); + const result = await help.execute({ args: [] }); + + expect(result.success).toBe(true); + expect(result.message).toContain("/clear (new) - Start a new session"); + expect(result.message).not.toContain("/new - Start a new session"); + }); +}); + +describe("Skill command prefixes", () => { + it("keeps non-command skills addressable without prompts prefix", async () => { + const result = await executeCommand("/example"); + + expect(result).not.toBeNull(); + expect(result?.success).toBe(true); + expect(result && "isSkill" in result && result.isSkill).toBe(true); + + if (result && "isSkill" in result && result.isSkill) { + expect(result.skillId).toBe("prompts:example"); + } + }); + + it("resolves prompts prefix for non-command skills", async () => { + const result = await executeCommand("/prompts:example"); + + expect(result).not.toBeNull(); + expect(result?.success).toBe(true); + expect(result && "isSkill" in result && result.isSkill).toBe(true); + + if (result && "isSkill" in result && result.isSkill) { + expect(result.skillId).toBe("prompts:example"); + } + }); +}); + +describe("Tool fallback command", () => { + let originalMode: ToolFallbackMode; + + beforeEach(() => { + originalMode = agentManager.getToolFallbackMode(); + }); + + afterEach(() => { + agentManager.setToolFallbackMode(originalMode); + }); + + it("shows mode usage when called without arguments", async () => { + const command = createToolFallbackCommand(); + const result = await command.execute({ args: [] }); + + expect(result.success).toBe(true); + expect(result.message).toContain("Tool fallback mode:"); + expect(result.message).toContain(TOOL_FALLBACK_MODES.join("|")); + }); + + it("sets explicit mode values", async () => { + const command = createToolFallbackCommand(); + + const result = await command.execute({ args: ["hermes"] }); + + expect(result.success).toBe(true); + expect(agentManager.getToolFallbackMode()).toBe("hermes"); + }); + + it("accepts legacy on/off values", async () => { + const command = createToolFallbackCommand(); + + await command.execute({ args: ["on"] }); + expect(agentManager.getToolFallbackMode()).toBe( + LEGACY_ENABLED_TOOL_FALLBACK_MODE + ); + + await command.execute({ args: ["off"] }); + expect(agentManager.getToolFallbackMode()).toBe(DEFAULT_TOOL_FALLBACK_MODE); + }); +}); diff --git a/src/commands/clear.ts b/src/commands/clear.ts index 58dea71..d2d0f25 100644 --- a/src/commands/clear.ts +++ b/src/commands/clear.ts @@ -1,18 +1,14 @@ -import type { MessageHistory } from "../context/message-history"; import type { Command, CommandResult } from "./types"; -export const createClearCommand = ( - messageHistory: MessageHistory -): Command => ({ +const newSessionAction = (): CommandResult => ({ + success: true, + action: "new-session", +}); + +export const createClearCommand = (): Command => ({ name: "clear", - description: "Clear current conversation history and terminal screen", - execute: (): CommandResult => { - messageHistory.clear(); - // Clear terminal screen (equivalent to Ctrl+L) - process.stdout.write("\x1b[2J\x1b[H"); - return { - success: true, - message: "Conversation history and terminal cleared.", - }; - }, + displayName: "clear (new)", + aliases: ["new"], + description: "Start a new session", + execute: () => newSessionAction(), }); diff --git a/src/commands/factories/create-toggle-command.ts b/src/commands/factories/create-toggle-command.ts index 1ecdf79..b5e541d 100644 --- a/src/commands/factories/create-toggle-command.ts +++ b/src/commands/factories/create-toggle-command.ts @@ -1,7 +1,7 @@ import { colorize } from "../../interaction/colors"; import type { Command, CommandResult } from "../types"; -export interface ToggleCommandConfig { +interface ToggleCommandConfig { description: string; disabledMessage?: string; enabledMessage?: string; diff --git a/src/commands/help.ts b/src/commands/help.ts index c5bb0d5..e20c802 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -6,8 +6,20 @@ export const createHelpCommand = ( name: "help", description: "Show available commands", execute: (): CommandResult => { + const getCommandName = (command: Command): string => { + if (command.displayName) { + return command.displayName; + } + + if (command.aliases && command.aliases.length > 0) { + return `${command.name} (${command.aliases.join(", ")})`; + } + + return command.name; + }; + const commandList = Array.from(getCommands().values()) - .map((cmd) => ` /${cmd.name} - ${cmd.description}`) + .map((cmd) => ` /${getCommandName(cmd)} - ${cmd.description}`) .join("\n"); return { diff --git a/src/commands/index.ts b/src/commands/index.ts index 1029300..fce3ace 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,4 +1,5 @@ -import { loadAllSkills, loadSkillById } from "../context/skills"; +import { toPromptsCommandName } from "../context/skill-command-prefix"; +import { loadSkillById } from "../context/skills"; import { createHelpCommand } from "./help"; import type { Command, CommandContext, CommandResult } from "./types"; @@ -9,13 +10,52 @@ export interface SkillCommandResult extends CommandResult { } const commands = new Map(); +const commandAliases = new Map(); const getCommands = (): Map => commands; export { getCommands }; export const registerCommand = (command: Command): void => { - commands.set(command.name, command); + const normalizedName = command.name.toLowerCase(); + + if (commands.has(normalizedName)) { + throw new Error(`Duplicate command name: ${normalizedName}`); + } + + const aliases = command.aliases?.map((alias) => alias.toLowerCase()) ?? []; + const normalizedCommand: Command = { + ...command, + name: normalizedName, + aliases, + }; + commands.set(normalizedName, normalizedCommand); + + for (const alias of aliases) { + if (alias === normalizedName) { + continue; + } + + if (commands.has(alias)) { + throw new Error( + `Alias '${alias}' for /${normalizedName} conflicts with command /${alias}` + ); + } + + const existingTarget = commandAliases.get(alias); + if (existingTarget && existingTarget !== normalizedName) { + throw new Error( + `Alias '${alias}' for /${normalizedName} already maps to /${existingTarget}` + ); + } + + commandAliases.set(alias, normalizedName); + } +}; + +export const resolveRegisteredCommandName = (name: string): string => { + const normalizedName = name.toLowerCase(); + return commandAliases.get(normalizedName) ?? normalizedName; }; registerCommand(createHelpCommand(getCommands)); @@ -52,7 +92,8 @@ export const executeCommand = async ( return null; } - const command = commands.get(parsed.name); + const resolvedName = resolveRegisteredCommandName(parsed.name); + const command = commands.get(resolvedName); if (!command) { // Check if it's a skill @@ -61,7 +102,7 @@ export const executeCommand = async ( return { success: true, isSkill: true, - skillId: skill.info.id, + skillId: toPromptsCommandName(skill.info.id), skillContent: skill.content, } as SkillCommandResult; } @@ -82,8 +123,3 @@ export const isSkillCommandResult = ( ): result is SkillCommandResult => { return result !== null && "isSkill" in result && result.isSkill === true; }; - -export const getAvailableSkillIds = async (): Promise => { - const skills = await loadAllSkills(); - return skills.map((s) => s.id); -}; diff --git a/src/commands/model.ts b/src/commands/model.ts index 6f983c1..b50c9e1 100644 --- a/src/commands/model.ts +++ b/src/commands/model.ts @@ -1,34 +1,40 @@ import type { ProviderType } from "../agent"; -import { ANTHROPIC_MODELS, agentManager, GEMINI_MODELS } from "../agent"; -import { env } from "../env"; +import { ANTHROPIC_MODELS, agentManager } from "../agent"; import { colorize } from "../interaction/colors"; -import { Spinner } from "../interaction/spinner"; import type { Command, CommandResult } from "./types"; -interface ModelInfo { +export interface ModelInfo { id: string; name?: string; provider: ProviderType; - status?: string; type?: "serverless" | "dedicated"; } -interface DedicatedEndpointData { - createdAt: string; - phase?: string; - status: string; - updatedAt: string; -} - -let cachedModels: ModelInfo[] | null = null; - -const VALID_DEDICATED_STATUSES = [ - "INITIALIZING", - "RUNNING", - "UPDATING", - "SLEEPING", - "AWAKING", - "READY", +const FRIENDLI_MODELS: readonly ModelInfo[] = [ + { + id: "MiniMaxAI/MiniMax-M2.5", + name: "MiniMax M2.5", + provider: "friendli", + type: "serverless", + }, + { + id: "MiniMaxAI/MiniMax-M2.1", + name: "MiniMax M2.1", + provider: "friendli", + type: "serverless", + }, + { + id: "zai-org/GLM-5", + name: "GLM 5", + provider: "friendli", + type: "serverless", + }, + { + id: "zai-org/GLM-4.7", + name: "GLM 4.7", + provider: "friendli", + type: "serverless", + }, ] as const; function getAnthropicModels(): ModelInfo[] { @@ -39,112 +45,71 @@ function getAnthropicModels(): ModelInfo[] { })); } -function getGeminiModels(): ModelInfo[] { - return GEMINI_MODELS.map((m) => ({ - id: m.id, - name: m.name, - provider: "gemini" as const, - })); -} - -async function fetchServerlessModels(): Promise { - if (!env.FRIENDLI_TOKEN) { - return []; - } +export function getAvailableModels(): ModelInfo[] { + const anthropicModels = getAnthropicModels(); - const response = await fetch("https://api.friendli.ai/serverless/v1/models", { - headers: { - Authorization: `Bearer ${env.FRIENDLI_TOKEN}`, - }, - }); + return [...anthropicModels, ...FRIENDLI_MODELS]; +} - if (!response.ok) { - throw new Error(`Failed to fetch serverless models: ${response.status}`); +export function findModelBySelection( + selection: string, + models: ModelInfo[] = getAvailableModels() +): ModelInfo | undefined { + const selectedIndex = Number.parseInt(selection, 10) - 1; + + if ( + !Number.isNaN(selectedIndex) && + selectedIndex >= 0 && + selectedIndex < models.length + ) { + return models[selectedIndex]; } - const data = (await response.json()) as { data: { id: string }[] }; - return data.data.map((m) => ({ - id: m.id, - type: "serverless" as const, - provider: "friendli" as const, - })); + return models.find((m) => m.id === selection); } -async function fetchDedicatedEndpoints(): Promise { - if (!env.FRIENDLI_TOKEN) { - return []; +const getProviderLabel = (provider: ProviderType): string => { + const providerLabels: Record = { + anthropic: "Anthropic", + friendli: "FriendliAI", + }; + return providerLabels[provider]; +}; + +export const applyModelSelection = ( + selectedModel: ModelInfo +): CommandResult => { + const currentModelId = agentManager.getModelId(); + const currentProvider = agentManager.getProvider(); + + if ( + selectedModel.id === currentModelId && + selectedModel.provider === currentProvider + ) { + return { + success: true, + message: `Already using model: ${selectedModel.id}`, + }; } - try { - const allEndpoints: ModelInfo[] = []; - let cursor: string | null = null; - - do { - const url = new URL("https://api.friendli.ai/dedicated/beta/endpoint"); - url.searchParams.set("limit", "100"); - if (cursor) { - url.searchParams.set("cursor", cursor); - } - - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${env.FRIENDLI_TOKEN}`, - }, - }); - - if (!response.ok) { - return allEndpoints; - } - - const data = (await response.json()) as { - data: Record; - nextCursor?: string | null; - }; - - const endpoints = Object.entries(data.data) - .filter(([_, endpoint]) => - VALID_DEDICATED_STATUSES.includes( - endpoint.status as (typeof VALID_DEDICATED_STATUSES)[number] - ) - ) - .map(([id, endpoint]) => ({ - id, - type: "dedicated" as const, - status: endpoint.status, - provider: "friendli" as const, - })); - - allEndpoints.push(...endpoints); - cursor = data.nextCursor ?? null; - } while (cursor); - - return allEndpoints; - } catch { - return []; + if (selectedModel.provider !== currentProvider) { + agentManager.setProvider(selectedModel.provider); } -} -async function fetchAvailableModels(): Promise { - if (cachedModels) { - return cachedModels; + agentManager.setModelId(selectedModel.id); + if (selectedModel.type) { + agentManager.setModelType(selectedModel.type); } - const anthropicModels = env.ANTHROPIC_API_KEY ? getAnthropicModels() : []; - const geminiModels = getGeminiModels(); - - const [serverlessModels, dedicatedEndpoints] = await Promise.all([ - fetchServerlessModels(), - fetchDedicatedEndpoints(), - ]); - - cachedModels = [ - ...anthropicModels, - ...geminiModels, - ...serverlessModels, - ...dedicatedEndpoints, - ]; - return cachedModels; -} + const providerLabel = getProviderLabel(selectedModel.provider); + return { + success: true, + message: colorize( + "green", + `Model changed to: ${selectedModel.id} (${providerLabel})` + ), + }; +}; function formatModelList( models: ModelInfo[], @@ -158,18 +123,13 @@ function formatModelList( let providerLabel: string; if (model.provider === "anthropic") { providerLabel = colorize("magenta", " [Anthropic]"); - } else if (model.provider === "gemini") { - providerLabel = colorize("yellow", " [Gemini]"); } else if (model.type === "dedicated") { providerLabel = colorize("cyan", " [FDE]"); } else { providerLabel = colorize("blue", " [FriendliAI]"); } - const statusLabel = model.status - ? colorize("yellow", ` (${model.status})`) - : ""; const nameLabel = model.name ? ` - ${model.name}` : ""; - return ` ${index + 1}. ${model.id}${nameLabel}${providerLabel}${statusLabel}${marker}`; + return ` ${index + 1}. ${model.id}${nameLabel}${providerLabel}${marker}`; }); return `Available models:\n${lines.join("\n")}\n\nUsage: /model to select`; @@ -178,87 +138,33 @@ function formatModelList( export const createModelCommand = (): Command => ({ name: "model", description: "List or change the AI model", - execute: async ({ args }): Promise => { - const spinner = new Spinner("Fetching available models..."); - - try { - spinner.start(); - const models = await fetchAvailableModels(); - spinner.stop(); - - if (models.length === 0) { - return { success: false, message: "No models available." }; - } - - const currentModelId = agentManager.getModelId(); - const currentProvider = agentManager.getProvider(); - - if (args.length === 0) { - return { - success: true, - message: formatModelList(models, currentModelId, currentProvider), - }; - } + execute: ({ args }): CommandResult => { + const models = getAvailableModels(); - const selection = args[0]; - const selectedIndex = Number.parseInt(selection, 10) - 1; - - let selectedModel: ModelInfo | undefined; - - if ( - !Number.isNaN(selectedIndex) && - selectedIndex >= 0 && - selectedIndex < models.length - ) { - selectedModel = models[selectedIndex]; - } else { - selectedModel = models.find((m) => m.id === selection); - } + if (models.length === 0) { + return { success: false, message: "No models available." }; + } - if (!selectedModel) { - return { - success: false, - message: `Invalid selection: ${selection}`, - }; - } + const currentModelId = agentManager.getModelId(); + const currentProvider = agentManager.getProvider(); - if ( - selectedModel.id === currentModelId && - selectedModel.provider === currentProvider - ) { - return { - success: true, - message: `Already using model: ${selectedModel.id}`, - }; - } + if (args.length === 0) { + return { + success: true, + message: formatModelList(models, currentModelId, currentProvider), + }; + } - // Set provider first (this will also set a default model for the provider) - if (selectedModel.provider !== currentProvider) { - agentManager.setProvider(selectedModel.provider); - } - // Then set the specific model - agentManager.setModelId(selectedModel.id); - if (selectedModel.type) { - agentManager.setModelType(selectedModel.type); - } + const selection = args[0]; + const selectedModel = findModelBySelection(selection, models); - const providerLabels: Record = { - anthropic: "Anthropic", - gemini: "Gemini", - friendli: "FriendliAI", - }; - const providerLabel = providerLabels[selectedModel.provider]; + if (!selectedModel) { return { - success: true, - message: colorize( - "green", - `Model changed to: ${selectedModel.id} (${providerLabel})` - ), + success: false, + message: `Invalid selection: ${selection}`, }; - } catch (error) { - spinner.stop(); - const message = error instanceof Error ? error.message : String(error); - return { success: false, message: `Error: ${message}` }; } + + return applyModelSelection(selectedModel); }, }); diff --git a/src/commands/render.ts b/src/commands/render.ts index aa660f0..235ae1a 100644 --- a/src/commands/render.ts +++ b/src/commands/render.ts @@ -6,6 +6,7 @@ import { env } from "../env"; import { colors } from "../interaction/colors"; import { Spinner } from "../interaction/spinner"; import { buildMiddlewares } from "../middleware"; +import type { ToolFallbackMode } from "../tool-fallback-mode"; import type { Command, CommandResult } from "./types"; interface RenderData { @@ -14,7 +15,7 @@ interface RenderData { model: string; modelType: ModelType; thinkingEnabled: boolean; - toolFallbackEnabled: boolean; + toolFallbackMode: ToolFallbackMode; tools: ToolSet; } @@ -37,7 +38,7 @@ async function renderChatPrompt({ tools, messages, thinkingEnabled, - toolFallbackEnabled, + toolFallbackMode, }: RenderData): Promise { const isDedicated = modelType === "dedicated"; const baseURL = isDedicated @@ -121,7 +122,7 @@ async function renderChatPrompt({ model: wrapLanguageModel({ model: friendli(model), middleware: buildMiddlewares({ - enableToolFallback: toolFallbackEnabled, + toolFallbackMode, }), }), system: instructions, diff --git a/src/commands/tool-fallback.ts b/src/commands/tool-fallback.ts index eb02d8a..f6c6788 100644 --- a/src/commands/tool-fallback.ts +++ b/src/commands/tool-fallback.ts @@ -1,15 +1,47 @@ import { agentManager } from "../agent"; -import { createToggleCommand } from "./factories/create-toggle-command"; +import { + parseToolFallbackMode, + TOOL_FALLBACK_MODES, +} from "../tool-fallback-mode"; import type { Command } from "./types"; -export const createToolFallbackCommand = (): Command => - createToggleCommand({ - name: "tool-fallback", - description: - "Toggle tool call fallback mode for models without native tool support", - getter: () => agentManager.isToolFallbackEnabled(), - setter: (value) => agentManager.setToolFallbackEnabled(value), - featureName: "Tool fallback", - enabledMessage: "Tool fallback enabled (using XML-based tool calling)", - disabledMessage: "Tool fallback disabled (using native tool support)", - }); +const TOOL_FALLBACK_USAGE = TOOL_FALLBACK_MODES.join("|"); + +export const createToolFallbackCommand = (): Command => ({ + name: "tool-fallback", + description: + "Set tool call fallback mode for models without native tool support", + argumentSuggestions: [...TOOL_FALLBACK_MODES], + execute: ({ args }) => { + if (args.length === 0) { + const currentMode = agentManager.getToolFallbackMode(); + return { + success: true, + message: `Tool fallback mode: ${currentMode}\nUsage: /tool-fallback <${TOOL_FALLBACK_USAGE}>`, + }; + } + + const rawMode = args[0] ?? ""; + const mode = parseToolFallbackMode(rawMode); + if (!mode) { + return { + success: false, + message: `Invalid mode: ${rawMode}. Use one of: ${TOOL_FALLBACK_USAGE}`, + }; + } + + const currentMode = agentManager.getToolFallbackMode(); + if (mode === currentMode) { + return { + success: true, + message: `Already using tool fallback mode: ${mode}`, + }; + } + + agentManager.setToolFallbackMode(mode); + return { + success: true, + message: `Tool fallback mode set to: ${mode}`, + }; + }, +}); diff --git a/src/commands/types.ts b/src/commands/types.ts index 03a63e6..7bd5b0d 100644 --- a/src/commands/types.ts +++ b/src/commands/types.ts @@ -3,13 +3,16 @@ export interface CommandContext { } export interface CommandResult { + action?: "new-session"; message?: string; success: boolean; } export interface Command { + aliases?: string[]; argumentSuggestions?: string[]; description: string; + displayName?: string; execute: (context: CommandContext) => CommandResult | Promise; name: string; } diff --git a/src/context/message-history.test.ts b/src/context/message-history.test.ts index 8255410..d248686 100644 --- a/src/context/message-history.test.ts +++ b/src/context/message-history.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test"; import type { AssistantModelMessage, TextPart, ToolCallPart } from "ai"; +import { MessageHistory } from "./message-history"; const TRAILING_NEWLINES = /\n+$/; @@ -139,3 +140,22 @@ describe("trimTrailingNewlines", () => { }); }); }); + +describe("MessageHistory", () => { + it("always trims trailing newlines when storing assistant messages", () => { + const history = new MessageHistory(); + history.addModelMessages([ + { + role: "assistant", + content: "Saved without trailing newlines\n\n", + }, + ]); + + expect(history.toModelMessages()).toEqual([ + { + role: "assistant", + content: "Saved without trailing newlines", + }, + ]); + }); +}); diff --git a/src/context/message-history.ts b/src/context/message-history.ts index 31aa0ea..19c6daf 100644 --- a/src/context/message-history.ts +++ b/src/context/message-history.ts @@ -1,5 +1,4 @@ import type { ModelMessage, TextPart } from "ai"; -import { env } from "../env"; const TRAILING_NEWLINES = /\n+$/; @@ -42,13 +41,9 @@ function trimTrailingNewlines(message: ModelMessage): ModelMessage { return message; } - // Create a new array with proper type preservation - const newContent = content.map((part, idx) => { - if (idx === lastTextIndex) { - return { type: "text" as const, text: trimmedText }; - } - return part; - }); + // Create a new array preserving all fields (e.g. providerOptions) + const newContent = [...content]; + newContent[lastTextIndex] = { ...textPart, text: trimmedText }; return { ...message, content: newContent }; } @@ -94,9 +89,7 @@ export class MessageHistory { addModelMessages(messages: ModelMessage[]): Message[] { const created: Message[] = []; for (const modelMessage of messages) { - const processedMessage = env.EXPERIMENTAL_TRIM_TRAILING_NEWLINES - ? trimTrailingNewlines(modelMessage) - : modelMessage; + const processedMessage = trimTrailingNewlines(modelMessage); // Serialize Error objects in tool results to prevent schema validation errors const sanitizedMessage = this.sanitizeMessage(processedMessage); diff --git a/src/context/skill-command-prefix.ts b/src/context/skill-command-prefix.ts new file mode 100644 index 0000000..f6aba5a --- /dev/null +++ b/src/context/skill-command-prefix.ts @@ -0,0 +1,18 @@ +export const PROMPTS_COMMAND_PREFIX = "prompts:"; + +export const toPromptsCommandName = (skillId: string): string => { + if (skillId.startsWith(PROMPTS_COMMAND_PREFIX)) { + return skillId; + } + + return `${PROMPTS_COMMAND_PREFIX}${skillId}`; +}; + +export const parsePromptsCommandName = (commandName: string): string | null => { + if (!commandName.startsWith(PROMPTS_COMMAND_PREFIX)) { + return null; + } + + const skillId = commandName.slice(PROMPTS_COMMAND_PREFIX.length); + return skillId.length > 0 ? skillId : null; +}; diff --git a/src/context/skills-integration.test.ts b/src/context/skills-integration.test.ts index e5880c9..c2db905 100644 --- a/src/context/skills-integration.test.ts +++ b/src/context/skills-integration.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "bun:test"; import { executeLoadSkill } from "../tools/planning/load-skill"; +import { + parsePromptsCommandName, + toPromptsCommandName, +} from "./skill-command-prefix"; import { loadAllSkills, loadSkillById } from "./skills"; describe("Skills Integration Tests", () => { @@ -87,4 +91,23 @@ describe("Skills Integration Tests", () => { ); expect(result).toContain("Only v2 skills support subdirectory files"); }); + + test("formats prompts-prefixed slash command names", () => { + expect(toPromptsCommandName("rams")).toBe("prompts:rams"); + expect(toPromptsCommandName("prompts:rams")).toBe("prompts:rams"); + }); + + test("parses prompts-prefixed slash command names", () => { + expect(parsePromptsCommandName("prompts:rams")).toBe("rams"); + expect(parsePromptsCommandName("prompts:")).toBeNull(); + expect(parsePromptsCommandName("rams")).toBeNull(); + }); + + test("loads v2 skill by prompts-prefixed name", async () => { + const result = await loadSkillById("prompts:example"); + + expect(result).toBeTruthy(); + expect(result?.info.id).toBe("example"); + expect(result?.info.format).toBe("v2"); + }); }); diff --git a/src/context/skills.ts b/src/context/skills.ts index 43f1b15..482bb26 100644 --- a/src/context/skills.ts +++ b/src/context/skills.ts @@ -6,6 +6,7 @@ import { cwd } from "node:process"; import { fileURLToPath } from "node:url"; import { glob } from "glob"; import { parse as parseYAML } from "yaml"; +import { parsePromptsCommandName } from "./skill-command-prefix"; const __dirname = dirname(fileURLToPath(import.meta.url)); const BUNDLED_SKILLS_DIR = join(__dirname, "../skills"); @@ -455,7 +456,11 @@ export async function loadSkillById( skillId: string ): Promise<{ content: string; info: SkillInfo } | null> { const allSkills = await loadAllSkills(); - const skill = allSkills.find((s) => s.id === skillId); + + const promptsSkillId = parsePromptsCommandName(skillId); + const skill = promptsSkillId + ? allSkills.find((s) => s.id === promptsSkillId) + : allSkills.find((s) => s.id === skillId); if (!skill) { return null; diff --git a/src/entrypoints/cli.ts b/src/entrypoints/cli.ts index 6dcbdae..f49f749 100644 --- a/src/entrypoints/cli.ts +++ b/src/entrypoints/cli.ts @@ -1,8 +1,28 @@ #!/usr/bin/env bun -import type { Interface as ReadlineInterface } from "node:readline"; -import { createInterface } from "node:readline"; import { stripVTControlCharacters } from "node:util"; +import { + type AutocompleteItem, + type AutocompleteProvider, + CombinedAutocompleteProvider, + Container, + Editor, + type EditorTheme, + Input, + isKeyRelease, + isKeyRepeat, + Key, + Loader, + Markdown, + type MarkdownTheme, + matchesKey, + ProcessTerminal, + SelectList, + type SlashCommand, + Spacer, + Text, + TUI, +} from "@mariozechner/pi-tui"; import type { ProviderType } from "../agent"; import { agentManager } from "../agent"; import { @@ -10,1458 +30,1218 @@ import { getCommands, isCommand, isSkillCommandResult, + parseCommand, registerCommand, + resolveRegisteredCommandName, } from "../commands"; import { createClearCommand } from "../commands/clear"; -import { createModelCommand } from "../commands/model"; +import { + applyModelSelection, + createModelCommand, + findModelBySelection, + getAvailableModels, + type ModelInfo, +} from "../commands/model"; import { createRenderCommand } from "../commands/render"; import { createThinkCommand } from "../commands/think"; import { createToolFallbackCommand } from "../commands/tool-fallback"; import { MessageHistory } from "../context/message-history"; -import { initializeSession } from "../context/session"; +import { getSessionId, initializeSession } from "../context/session"; +import { toPromptsCommandName } from "../context/skill-command-prefix"; import type { SkillInfo } from "../context/skills"; import { loadAllSkills } from "../context/skills"; import { env } from "../env"; -import { colorize } from "../interaction/colors"; -import { StdinBuffer } from "../interaction/stdin-buffer"; -import { renderFullStream } from "../interaction/stream-renderer"; +import { renderFullStreamWithPiTui } from "../interaction/pi-tui-stream-renderer"; +import { setSpinnerOutputEnabled } from "../interaction/spinner"; +import { + MANUAL_TOOL_LOOP_MAX_STEPS, + shouldContinueManualToolLoop, +} from "../interaction/tool-loop-control"; import { buildTodoContinuationUserMessage, getIncompleteTodos, } from "../middleware/todo-continuation"; +import { + DEFAULT_TOOL_FALLBACK_MODE, + LEGACY_ENABLED_TOOL_FALLBACK_MODE, + parseToolFallbackMode, + TOOL_FALLBACK_MODES, + type ToolFallbackMode, +} from "../tool-fallback-mode"; import { cleanupSession } from "../tools/execute/shared-tmux-session"; import { initializeTools } from "../utils/tools-manager"; -// Bracketed paste mode escape sequences -const PASTE_START = "\x1b[200~"; -const PASTE_END = "\x1b[201~"; -// Enable/disable bracketed paste mode -const ENABLE_BRACKETED_PASTE = "\x1b[?2004h"; -const DISABLE_BRACKETED_PASTE = "\x1b[?2004l"; -// Regex patterns for line ending normalization -const LINE_ENDING_REGEX = /\r\n|\r|\n/g; - -// ANSI escape codes for styling -const ANSI_DIM = "\x1b[90m"; -const ANSI_CYAN = "\x1b[36m"; const ANSI_RESET = "\x1b[0m"; -const ANSI_CURSOR_UP = (n: number) => `\x1b[${n}A`; -const ANSI_CURSOR_DOWN = (n: number) => `\x1b[${n}B`; -const ANSI_CURSOR_FORWARD = (n: number) => `\x1b[${n}C`; -const ANSI_CLEAR_TO_END = "\x1b[J"; +const ANSI_BOLD = "\x1b[1m"; +const ANSI_DIM = "\x1b[2m"; +const ANSI_ITALIC = "\x1b[3m"; +const ANSI_UNDERLINE = "\x1b[4m"; +const ANSI_BG_GRAY = "\x1b[100m"; +const ANSI_GREEN = "\x1b[92m"; +const ANSI_YELLOW = "\x1b[93m"; +const ANSI_MAGENTA = "\x1b[95m"; +const ANSI_CYAN = "\x1b[36m"; +const ANSI_BRIGHT_CYAN = "\x1b[96m"; +const ANSI_GRAY = "\x1b[90m"; +const CTRL_C_ETX = "\u0003"; +const CTRL_C_EXIT_WINDOW_MS = 500; const messageHistory = new MessageHistory(); - -let rlInstance: ReadlineInterface | null = null; -let shouldExit = false; let cachedSkills: SkillInfo[] = []; -const commandHistory: string[] = []; // Store command history - -const TODO_CONTINUATION_MAX_LOOPS = 5; - -process.on("exit", () => { - if (env.DEBUG_TMUX_CLEANUP) { - console.error("[DEBUG] Process exit handler called"); - } - cleanupSession(); -}); - -registerCommand( - createRenderCommand(async () => ({ - model: agentManager.getModelId(), - modelType: agentManager.getModelType(), - instructions: await agentManager.getInstructions(), - tools: agentManager.getTools(), - messages: messageHistory.toModelMessages(), - thinkingEnabled: agentManager.isThinkingEnabled(), - toolFallbackEnabled: agentManager.isToolFallbackEnabled(), - })) -); -registerCommand(createModelCommand()); -registerCommand(createClearCommand(messageHistory)); -registerCommand(createThinkCommand()); -registerCommand(createToolFallbackCommand()); - -const processAgentResponse = async (_rl: ReadlineInterface): Promise => { - const stream = await agentManager.stream(messageHistory.toModelMessages()); - await renderFullStream(stream.fullStream, { - showSteps: false, - }); - - const response = await stream.response; - messageHistory.addModelMessages(response.messages); -}; - -const parseCliArgs = (): { - thinking: boolean; - toolFallback: boolean; - model: string | null; - provider: ProviderType | null; -} => { - const args = process.argv.slice(2); - let thinking = false; - let toolFallback = false; - let model: string | null = null; - let provider: ProviderType | null = null; +let shouldExit = false; +let activeStreamController: AbortController | null = null; +let streamInterruptRequested = false; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === "--think") { - thinking = true; - } else if (arg === "--tool-fallback") { - toolFallback = true; - } else if (arg === "--model" && i + 1 < args.length) { - model = args[i + 1]; - i++; - } else if (arg === "--provider" && i + 1 < args.length) { - const providerArg = args[i + 1]; - if (providerArg === "anthropic" || providerArg === "friendli") { - provider = providerArg; - } - i++; - } +const cancelActiveStream = (): boolean => { + if (!activeStreamController || activeStreamController.signal.aborted) { + return false; } - return { thinking, toolFallback, model, provider }; + streamInterruptRequested = true; + activeStreamController.abort("User requested stream interruption"); + return true; }; -const handleGracefulShutdown = () => { - shouldExit = true; - console.log("\nShutting down..."); - - if (rlInstance) { - rlInstance.close(); +const clearActiveStreamController = ( + streamController: AbortController +): void => { + if (activeStreamController === streamController) { + activeStreamController = null; } - - cleanupSession(); - process.exit(0); }; -const shouldExitFromInput = (input: string): boolean => { - return shouldExit || input.length === 0 || input.toLowerCase() === "exit"; +const style = (prefix: string, text: string): string => { + return `${prefix}${text}${ANSI_RESET}`; }; -const handleAgentResponse = async (rl: ReadlineInterface): Promise => { - try { - let continuationCount = 0; - - while (continuationCount <= TODO_CONTINUATION_MAX_LOOPS) { - await processAgentResponse(rl); - - const incompleteTodos = await getIncompleteTodos(); - if (incompleteTodos.length === 0) { - return; - } - - if (continuationCount === TODO_CONTINUATION_MAX_LOOPS) { - console.log( - colorize( - "yellow", - "[todo] Auto-continue limit reached; waiting for input." - ) - ); - return; - } - - const reminder = buildTodoContinuationUserMessage(incompleteTodos); - messageHistory.addUserMessage(reminder); - continuationCount += 1; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`\nError: ${errorMessage}`); - console.error("Returning to prompt...\n"); - } +const sanitizeCodeFence = (text: string): string => { + return text.replaceAll("```", "` ` `"); }; -// Control character codes -const CTRL_A = 1; -const CTRL_B = 2; -const CTRL_C = 3; -const CTRL_D = 4; -const CTRL_E = 5; -const CTRL_F = 6; -const TAB = 9; -const LF = 10; -const CTRL_K = 11; -const CTRL_U = 21; -const CTRL_W = 23; -const CR = 13; -const BACKSPACE_1 = 8; -const BACKSPACE_2 = 127; - -const WHITESPACE_REGEX = /\s/; -const ZERO_WIDTH_CODEPOINTS = new Set([0x20_0d, 0xfe_0e, 0xfe_0f]); - -const stripVtControlCharacters = - typeof stripVTControlCharacters === "function" - ? stripVTControlCharacters - : null; - -// Try to get getStringWidth from node:util if available -// This is an experimental feature, so we check for it at runtime -let getNodeStringWidth: ((str: string) => number) | null = null; -try { - const nodeUtil = require("node:util"); - if (typeof nodeUtil.getStringWidth === "function") { - getNodeStringWidth = nodeUtil.getStringWidth; +const stripAnsi = (value: string): string => { + if (typeof stripVTControlCharacters === "function") { + return stripVTControlCharacters(value); } -} catch { - // getStringWidth not available -} - -const graphemeSegmenter = - typeof Intl !== "undefined" && "Segmenter" in Intl - ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) - : null; - -interface Suggestion { - description: string; - value: string; -} - -interface InputState { - buffer: string; - cursor: number; - historyIndex: number; - lastCursorRow: number; - lastInputRows: number; - lastSuggestionRows: number; - originalBuffer: string; - suggestionIndex: number; - suggestions: Suggestion[]; -} -type InputAction = "submit" | "cancel" | "continue"; - -type EscapeAction = - | "left" - | "right" - | "up" - | "down" - | "home" - | "end" - | "delete" - | "word-left" - | "word-right" - | "delete-word-left" - | "delete-word-right" - | "line-start" - | "line-end"; - -type InputToken = - | { type: "text"; value: string; length: number } - | { type: "escape"; action: EscapeAction; length: number } - | { type: "paste-start"; length: number } - | { type: "paste-end"; length: number } - | { type: "ignore"; length: number }; - -const stripAnsi = (input: string): string => { let output = ""; - let idx = 0; - - while (idx < input.length) { - const char = input[idx]; - if (char === "\u001b" || char === "\u009b") { - if (char === "\u001b" && input[idx + 1] === "[") { - idx += 2; - } else { - idx += 1; - } - - while (idx < input.length) { - const code = input.charCodeAt(idx); - idx += 1; - if (code >= 0x40 && code <= 0x7e) { - break; + let index = 0; + + while (index < value.length) { + const char = value[index]; + if (char === "\u001b") { + index += 1; + if (index < value.length && value[index] === "[") { + index += 1; + while (index < value.length) { + const code = value.charCodeAt(index); + index += 1; + if (code >= 0x40 && code <= 0x7e) { + break; + } } } continue; } output += char; - idx += 1; + index += 1; } return output; }; -const splitGraphemes = (input: string): string[] => { - if (!graphemeSegmenter) { - return Array.from(input); - } - return Array.from( - graphemeSegmenter.segment(input), - (segment) => segment.segment - ); -}; - -const normalizeLineEndings = (input: string): string => - input.replace(LINE_ENDING_REGEX, "\n"); - -const isCombiningCodePoint = (codePoint: number): boolean => - (codePoint >= 0x03_00 && codePoint <= 0x03_6f) || - (codePoint >= 0x1a_b0 && codePoint <= 0x1a_ff) || - (codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || - (codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || - (codePoint >= 0xfe_20 && codePoint <= 0xfe_2f); - -const isFullwidthCodePoint = (codePoint: number): boolean => - codePoint >= 0x11_00 && - (codePoint <= 0x11_5f || - codePoint === 0x23_29 || - codePoint === 0x23_2a || - (codePoint >= 0x2e_80 && codePoint <= 0xa4_cf && codePoint !== 0x30_3f) || - (codePoint >= 0xac_00 && codePoint <= 0xd7_a3) || - (codePoint >= 0xf9_00 && codePoint <= 0xfa_ff) || - (codePoint >= 0xfe_10 && codePoint <= 0xfe_19) || - (codePoint >= 0xfe_30 && codePoint <= 0xfe_6f) || - (codePoint >= 0xff_00 && codePoint <= 0xff_60) || - (codePoint >= 0xff_e0 && codePoint <= 0xff_e6) || - (codePoint >= 0x1_f3_00 && codePoint <= 0x1_f6_4f) || - (codePoint >= 0x1_f9_00 && codePoint <= 0x1_f9_ff) || - (codePoint >= 0x2_00_00 && codePoint <= 0x3_ff_fd)); - -const getCodePointWidth = (char: string): number => { - const codePoint = char.codePointAt(0); - if (codePoint === undefined) { - return 0; - } - if (codePoint <= 31 || (codePoint >= 127 && codePoint <= 159)) { - return 0; - } - if (ZERO_WIDTH_CODEPOINTS.has(codePoint)) { - return 0; - } - if (isCombiningCodePoint(codePoint)) { - return 0; - } - if (isFullwidthCodePoint(codePoint)) { - return 2; - } - return 1; +const createMarkdownTheme = (): MarkdownTheme => { + return { + heading: (text) => style(`${ANSI_BOLD}${ANSI_BRIGHT_CYAN}`, text), + link: (text) => style(`${ANSI_UNDERLINE}${ANSI_CYAN}`, text), + linkUrl: (text) => style(ANSI_GRAY, text), + code: (text) => style(ANSI_YELLOW, text), + codeBlock: (text) => style(ANSI_GREEN, text), + codeBlockBorder: (text) => style(ANSI_GRAY, text), + quote: (text) => style(`${ANSI_ITALIC}${ANSI_GRAY}`, text), + quoteBorder: (text) => style(ANSI_GRAY, text), + hr: (text) => style(ANSI_GRAY, text), + listBullet: (text) => style(ANSI_MAGENTA, text), + bold: (text) => style(ANSI_BOLD, text), + italic: (text) => style(ANSI_ITALIC, text), + strikethrough: (text) => style(ANSI_DIM, text), + underline: (text) => style(ANSI_UNDERLINE, text), + codeBlockIndent: " ", + }; }; -const getStringWidthFallback = (input: string): number => { - let width = 0; - for (const char of input) { - width += getCodePointWidth(char); - } - return width; +const createEditorTheme = (): EditorTheme => { + return { + borderColor: (text: string) => style(ANSI_GRAY, text), + selectList: { + selectedPrefix: (text: string) => style(`${ANSI_BOLD}${ANSI_CYAN}`, text), + selectedText: (text: string) => style(ANSI_CYAN, text), + description: (text: string) => style(ANSI_GRAY, text), + scrollInfo: (text: string) => style(ANSI_DIM, text), + noMatch: (text: string) => style(ANSI_DIM, text), + }, + }; }; -const getStringWidth = (input: string): number => { - const sanitized = stripVtControlCharacters - ? stripVtControlCharacters(input) - : input; - if (getNodeStringWidth) { - return getNodeStringWidth(sanitized); - } - return getStringWidthFallback(sanitized); +const addUserMessage = ( + chatContainer: Container, + markdownTheme: MarkdownTheme, + message: string +): void => { + chatContainer.addChild(new Spacer(1)); + chatContainer.addChild( + new Markdown(message, 1, 1, markdownTheme, { + bgColor: (text: string) => style(ANSI_BG_GRAY, text), + }) + ); }; -const isWhitespace = (value: string): boolean => WHITESPACE_REGEX.test(value); +const addSystemMessage = (chatContainer: Container, message: string): void => { + const cleaned = stripAnsi(message).trimEnd(); -const findLineStart = (graphemes: string[], cursor: number): number => { - for (let i = cursor - 1; i >= 0; i -= 1) { - if (graphemes[i] === "\n") { - return i + 1; - } + if (cleaned.length === 0) { + return; } - return 0; + + chatContainer.addChild(new Spacer(1)); + chatContainer.addChild( + new Text(style(ANSI_GRAY, sanitizeCodeFence(cleaned)), 1, 0) + ); }; -const findLineEnd = (graphemes: string[], cursor: number): number => { - for (let i = cursor; i < graphemes.length; i += 1) { - if (graphemes[i] === "\n") { - return i; - } - } - return graphemes.length; +const addNewSessionMessage = (chatContainer: Container): void => { + chatContainer.addChild(new Spacer(1)); + chatContainer.addChild( + new Text(style(ANSI_BRIGHT_CYAN, "✓ New session started"), 1, 1) + ); }; -const MAX_VISIBLE_SUGGESTIONS = 8; -const MIN_DESCRIPTION_LENGTH = 20; -const SUGGESTION_PADDING = 10; - -/** - * Create a suggestion object for a command argument. - */ -const createArgumentSuggestion = ( - commandName: string, - arg: string -): Suggestion => ({ - value: `/${commandName} ${arg}`, - description: `Argument: ${arg}`, -}); +const SKILL_LABEL_MAX_WIDTH = 24; +const MODEL_LABEL_MAX_WIDTH = 26; +const MODEL_NAME_MAX_WIDTH = 28; -/** - * Determine if the suggestion list should be displayed. - */ -const shouldDisplaySuggestionList = ( - state: InputState, - cursorAtEnd: boolean -): boolean => { - if (state.suggestions.length === 0) { - return false; - } - if (!cursorAtEnd) { - return false; +type SlashAutocompleteEntry = SlashCommand | AutocompleteItem; + +const truncateAutocompleteLabel = (value: string, maxWidth: number): string => { + if (value.length <= maxWidth) { + return value; } - if (state.buffer.length === 0) { - return false; + + if (maxWidth <= 1) { + return "…"; } - // Hide list when the only suggestion exactly matches the buffer - const isFullyTyped = - state.suggestions.length === 1 && - state.suggestions[0].value === state.buffer; - return !isFullyTyped; -}; -/** - * Calculate the maximum description length based on terminal width. - * Returns a safe minimum even for very small terminals. - */ -const calculateMaxDescriptionLength = ( - columns: number, - valueLength: number -): number => { - const available = columns - valueLength - SUGGESTION_PADDING; - return Math.max(MIN_DESCRIPTION_LENGTH, available); + return `${value.slice(0, maxWidth - 1)}…`; }; -/** - * Truncate description to fit within the given max length. - */ -const truncateDescription = (description: string, maxLen: number): string => { - if (description.length <= maxLen) { - return description; - } - return `${description.slice(0, maxLen - 3)}...`; +const toAutocompleteEntryValue = (entry: SlashAutocompleteEntry): string => { + return "name" in entry ? entry.name : entry.value; }; -/** - * Calculate the scroll window for suggestion list display. - */ -const calculateScrollWindow = ( - total: number, - selectedIndex: number, - maxVisible: number -): { startIndex: number; endIndex: number } => { - let startIndex = 0; - if (total > maxVisible) { - const scrollPadding = Math.floor(maxVisible / 2); - startIndex = Math.max(0, selectedIndex - scrollPadding); - startIndex = Math.min(startIndex, total - maxVisible); +const buildModelSelectorDescription = ( + modelName: string | undefined, + providerLabel: string +): string => { + if (!modelName) { + return providerLabel; } - return { startIndex, endIndex: startIndex + maxVisible }; -}; -/** - * Render a single suggestion item. - */ -const renderSuggestionItem = ( - suggestion: Suggestion, - isSelected: boolean, - columns: number -): void => { - const prefix = isSelected ? `${ANSI_CYAN}› ` : " "; - const reset = isSelected ? ANSI_RESET : ""; - const maxDescLen = calculateMaxDescriptionLength( - columns, - suggestion.value.length - ); - const desc = truncateDescription(suggestion.description, maxDescLen); - process.stdout.write( - `${prefix}${suggestion.value}${ANSI_DIM} - ${desc}${ANSI_RESET}${reset}\n` + const truncatedName = truncateAutocompleteLabel( + modelName, + MODEL_NAME_MAX_WIDTH ); + return `${truncatedName} - ${providerLabel}`; }; -/** - * Render the suggestion list below the input. - * Returns the number of rows rendered. - */ -const renderSuggestionList = (state: InputState, columns: number): number => { - const total = state.suggestions.length; - const maxVisible = Math.min(MAX_VISIBLE_SUGGESTIONS, total); - const { startIndex, endIndex } = calculateScrollWindow( - total, - state.suggestionIndex, - maxVisible +const buildModelSelectorLabel = ( + modelId: string, + isCurrent: boolean +): string => { + const currentMarker = isCurrent ? "* " : " "; + const truncatedModelId = truncateAutocompleteLabel( + modelId, + MODEL_LABEL_MAX_WIDTH ); + return `${currentMarker}${truncatedModelId}`; +}; - process.stdout.write("\n"); - let rows = 1; +const buildCurrentIndicatorLabel = ( + label: string, + isCurrent: boolean +): string => { + const currentMarker = isCurrent ? "* " : " "; + return `${currentMarker}${label}`; +}; - // Show scroll indicator at top if not at beginning - if (startIndex > 0) { - process.stdout.write( - `${ANSI_DIM} ↑ ${startIndex} more above${ANSI_RESET}\n` - ); - rows++; - } +const createAutocompleteCommands = ( + skills: SkillInfo[] +): SlashAutocompleteEntry[] => { + const createCommandSuggestion = ( + command: { + argumentSuggestions?: string[]; + description: string; + name: string; + }, + name: string, + description: string + ): SlashCommand => { + const suggestions = command.argumentSuggestions; + + return { + name, + description, + getArgumentCompletions: + suggestions && suggestions.length > 0 + ? (argumentPrefix: string) => { + const matches = suggestions.filter((suggestion) => + suggestion + .toLowerCase() + .startsWith(argumentPrefix.toLowerCase()) + ); + + if (matches.length === 0) { + return null; + } + + return matches.map((match) => ({ + value: match, + label: match, + })); + } + : undefined, + } satisfies SlashCommand; + }; - // Render visible suggestions - for (let i = startIndex; i < endIndex; i++) { - renderSuggestionItem( - state.suggestions[i], - i === state.suggestionIndex, - columns - ); - rows++; - } + const commandSuggestions = Array.from(getCommands().values()).map( + (command) => { + const aliases = + "aliases" in command && Array.isArray(command.aliases) + ? command.aliases + : []; + const aliasSuffix = + aliases.length > 0 ? ` (aliases: ${aliases.join(", ")})` : ""; + + return createCommandSuggestion( + command, + command.name, + `${command.description}${aliasSuffix}` + ); + } + ); - // Show scroll indicator at bottom if not at end - if (endIndex < total) { - const remaining = total - endIndex; - process.stdout.write( - `${ANSI_DIM} ↓ ${remaining} more below${ANSI_RESET}\n` - ); - rows++; - } + const skillSuggestions: AutocompleteItem[] = skills.map((skill) => { + const commandName = toPromptsCommandName(skill.id); + return { + value: commandName, + label: truncateAutocompleteLabel(commandName, SKILL_LABEL_MAX_WIDTH), + description: skill.description, + }; + }); - return rows; -}; + const suggestions: SlashAutocompleteEntry[] = [ + ...commandSuggestions, + ...skillSuggestions, + ]; + const seenNames = new Set(); + const uniqueSuggestions: SlashAutocompleteEntry[] = []; -/** - * ============================================================================= - * INPUT RENDERING SYSTEM - SAFETY CRITICAL DOCUMENTATION - * ============================================================================= - * - * CORE PRINCIPLE: Never destroy content in the scrollback buffer. - * - * The terminal has two regions: - * 1. Scrollback buffer: Previously output text (model responses, etc.) - * 2. Input area: Current user input + suggestion list (managed by us) - * - * INVARIANT: We may ONLY modify the input area. The scrollback is READ-ONLY. - * - * STATE TRACKING: - * - lastInputRows: Number of rows the input text occupied in previous render - * - lastSuggestionRows: Number of rows the suggestion list occupied - * - lastCursorRow: Which row (0-indexed) within the input area the cursor was on - * - * SAFE NAVIGATION RULE: - * - To reach input area start: move UP by lastCursorRow (cursor's current row) - * - To clear input area: clear lastInputRows lines going DOWN - * - To clear suggestion area: continue DOWN and clear lastSuggestionRows lines - * - NEVER move UP more than lastCursorRow from current position - * - * EXAMPLE (cursor at row 0, input 1 line, suggestions 5 lines): - * [scrollback - DO NOT TOUCH] - * [input row 0] ← cursor here, lastCursorRow=0 - * [suggestion row 0] - * [suggestion row 1] - * ... - * - * To clear: move up 0, clear 1 line, move down & clear 5 lines - * - * PENDING WRAP HANDLING: - * - When output width exactly equals terminal columns, cursor may be in - * "pending wrap" state (still on current line, or already on next line) - * - Solution: Output " \x1B[D\x1B[K" (space, cursor-left, clear-to-end) - * This forces wrap and leaves cursor at known position (next line, col 0) - * - * ============================================================================= - */ - -const getInlineSuggestion = ( - state: InputState, - cursorAtEnd: boolean -): string => { - if ( - state.suggestions.length > 0 && - state.suggestionIndex < state.suggestions.length && - cursorAtEnd - ) { - const suggestion = state.suggestions[state.suggestionIndex]; - if (suggestion.value.toLowerCase().startsWith(state.buffer.toLowerCase())) { - return suggestion.value.slice(state.buffer.length); + for (const suggestion of suggestions) { + const normalizedName = toAutocompleteEntryValue(suggestion).toLowerCase(); + if (seenNames.has(normalizedName)) { + continue; } + + seenNames.add(normalizedName); + uniqueSuggestions.push(suggestion); } - return ""; + + return uniqueSuggestions; }; -const clearPreviousInputArea = ( - lastInputRows: number, - lastSuggestionRows: number, - lastCursorRow: number -): void => { - if (lastCursorRow > 0) { - process.stdout.write(ANSI_CURSOR_UP(lastCursorRow)); - } - process.stdout.write("\r"); +const toAutocompleteItem = (suggestion: SlashCommand): AutocompleteItem => ({ + value: suggestion.name, + label: suggestion.name, + ...(suggestion.description ? { description: suggestion.description } : {}), +}); + +const buildCommandSuggestionsByName = ( + slashCommands: SlashAutocompleteEntry[] +): Map => { + const commandSuggestionsByName = new Map(); - for (let i = 0; i < lastInputRows; i++) { - process.stdout.write("\x1B[K"); - if (i < lastInputRows - 1) { - process.stdout.write("\n"); + for (const suggestion of slashCommands) { + if (!("name" in suggestion)) { + continue; } - } - for (let i = 0; i < lastSuggestionRows; i++) { - process.stdout.write("\n"); - process.stdout.write("\x1B[K"); + commandSuggestionsByName.set(suggestion.name.toLowerCase(), suggestion); } - const totalMoved = lastInputRows - 1 + lastSuggestionRows; - if (totalMoved > 0) { - process.stdout.write(ANSI_CURSOR_UP(totalMoved)); - } - process.stdout.write("\r"); + return commandSuggestionsByName; }; -const writeInputContent = ( - prompt: string, - displayBuffer: string, - suggestionText: string, - fullWidth: number, - columns: number -): void => { - process.stdout.write(`${prompt}${displayBuffer}`); +const buildAliasToCanonicalNameMap = (): Map => { + const aliasToCanonicalName = new Map(); - if (suggestionText.length > 0) { - process.stdout.write(`${ANSI_DIM}${suggestionText}${ANSI_RESET}`); - } + for (const command of getCommands().values()) { + const canonicalName = command.name.toLowerCase(); + + for (const alias of command.aliases ?? []) { + const normalizedAlias = alias.toLowerCase(); + if (normalizedAlias === canonicalName) { + continue; + } - if (fullWidth > 0 && fullWidth % columns === 0) { - process.stdout.write(" \x1B[D\x1B[K"); + aliasToCanonicalName.set(normalizedAlias, canonicalName); + } } + + return aliasToCanonicalName; }; -const positionCursor = ( - promptWidth: number, - displayBuffer: string, - cursorPos: number, - fullWidth: number, - columns: number -): number => { - const graphemes = splitGraphemes(displayBuffer); - const beforeCursor = graphemes.slice(0, cursorPos).join(""); - const beforeCursorWidth = getStringWidth(beforeCursor); - const cursorTotalWidth = promptWidth + beforeCursorWidth; - const cursorRow = Math.floor(cursorTotalWidth / columns); - const cursorCol = cursorTotalWidth % columns; - - const currentRow = - fullWidth > 0 && fullWidth % columns === 0 - ? fullWidth / columns - : Math.floor(fullWidth / columns); - - if (currentRow > 0) { - process.stdout.write(ANSI_CURSOR_UP(currentRow)); +const getAliasArgumentSuggestions = ( + textBeforeCursor: string, + commandSuggestionsByName: Map +): { items: AutocompleteItem[]; prefix: string } | null => { + const spaceIndex = textBeforeCursor.indexOf(" "); + if (spaceIndex < 0) { + return null; + } + + const commandName = textBeforeCursor.slice(1, spaceIndex).toLowerCase(); + const resolvedName = resolveRegisteredCommandName(commandName); + if (resolvedName === commandName) { + return null; } - process.stdout.write("\r"); - if (cursorRow > 0) { - process.stdout.write(ANSI_CURSOR_DOWN(cursorRow)); + const command = commandSuggestionsByName.get(resolvedName); + if (!command?.getArgumentCompletions) { + return null; } - if (cursorCol > 0) { - process.stdout.write(ANSI_CURSOR_FORWARD(cursorCol)); + + const argumentPrefix = textBeforeCursor.slice(spaceIndex + 1); + const items = command.getArgumentCompletions(argumentPrefix); + if (!items || items.length === 0) { + return null; } - return cursorRow; + return { + items, + prefix: argumentPrefix, + }; }; -const renderInput = ( - state: InputState, - prompt: string, - promptPlain: string -): void => { - const columns = process.stdout.columns || 80; - const promptWidth = getStringWidth(promptPlain); - const cursorAtEnd = state.cursor === splitGraphemes(state.buffer).length; - - const suggestionText = getInlineSuggestion(state, cursorAtEnd); - const displayBuffer = state.buffer.replace(/\n/g, " "); - const fullWidth = - promptWidth + getStringWidth(displayBuffer + suggestionText); - - clearPreviousInputArea( - state.lastInputRows, - state.lastSuggestionRows, - state.lastCursorRow - ); +const getAliasMatches = ( + query: string, + aliasToCanonicalName: Map, + commandSuggestionsByName: Map +): AutocompleteItem[] => { + const aliasMatches: AutocompleteItem[] = []; + const seenCanonicalNames = new Set(); - writeInputContent(prompt, displayBuffer, suggestionText, fullWidth, columns); + for (const [alias, canonicalName] of aliasToCanonicalName) { + if (!alias.startsWith(query) || seenCanonicalNames.has(canonicalName)) { + continue; + } - const newInputRows = fullWidth === 0 ? 1 : Math.ceil(fullWidth / columns); - state.lastInputRows = newInputRows; + const suggestion = commandSuggestionsByName.get(canonicalName); + if (!suggestion) { + continue; + } - const shouldShowList = shouldDisplaySuggestionList(state, cursorAtEnd); - if (shouldShowList) { - const suggestionRows = renderSuggestionList(state, columns); - state.lastSuggestionRows = suggestionRows; - process.stdout.write(ANSI_CURSOR_UP(suggestionRows)); - process.stdout.write("\r"); - } else { - state.lastSuggestionRows = 0; + seenCanonicalNames.add(canonicalName); + aliasMatches.push(toAutocompleteItem(suggestion)); } - state.lastCursorRow = positionCursor( - promptWidth, - displayBuffer, - state.cursor, - fullWidth, - columns - ); + return aliasMatches; }; -const insertText = (state: InputState, text: string): void => { - if (text.length === 0) { - return; - } - const graphemes = splitGraphemes(state.buffer); - const insertGraphemes = splitGraphemes(text); - graphemes.splice(state.cursor, 0, ...insertGraphemes); - state.cursor += insertGraphemes.length; - state.buffer = graphemes.join(""); -}; +const mergeAutocompleteItems = ( + prioritizedItems: AutocompleteItem[], + fallbackItems: AutocompleteItem[] = [] +): AutocompleteItem[] => { + const mergedItems: AutocompleteItem[] = []; + const seenValues = new Set(); -const deleteBackward = (state: InputState): void => { - if (state.cursor === 0) { - return; - } - const graphemes = splitGraphemes(state.buffer); - graphemes.splice(state.cursor - 1, 1); - state.cursor -= 1; - state.buffer = graphemes.join(""); -}; + for (const item of [...prioritizedItems, ...fallbackItems]) { + const normalizedValue = item.value.toLowerCase(); + if (seenValues.has(normalizedValue)) { + continue; + } -const deleteForward = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - if (state.cursor >= graphemes.length) { - return; + seenValues.add(normalizedValue); + mergedItems.push(item); } - graphemes.splice(state.cursor, 1); - state.buffer = graphemes.join(""); -}; -const moveCursorLeft = (state: InputState): void => { - if (state.cursor > 0) { - state.cursor -= 1; - } + return mergedItems; }; -const moveCursorRight = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - if (state.cursor < graphemes.length) { - state.cursor += 1; - } -}; +const createAliasAwareAutocompleteProvider = ( + skills: SkillInfo[] +): AutocompleteProvider => { + const slashCommands = createAutocompleteCommands(skills); + const fallbackProvider = new CombinedAutocompleteProvider( + slashCommands, + process.cwd() + ); + const commandSuggestionsByName = buildCommandSuggestionsByName(slashCommands); + const aliasToCanonicalName = buildAliasToCanonicalNameMap(); -const moveWordLeft = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - let idx = state.cursor; - while (idx > 0 && isWhitespace(graphemes[idx - 1])) { - idx -= 1; - } - while (idx > 0 && !isWhitespace(graphemes[idx - 1])) { - idx -= 1; - } - state.cursor = idx; -}; + return { + getSuggestions: (lines, cursorLine, cursorCol) => { + const currentLine = lines[cursorLine] ?? ""; + const textBeforeCursor = currentLine.slice(0, cursorCol); -const moveWordRight = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - let idx = state.cursor; - while (idx < graphemes.length && isWhitespace(graphemes[idx])) { - idx += 1; - } - while (idx < graphemes.length && !isWhitespace(graphemes[idx])) { - idx += 1; - } - state.cursor = idx; -}; + if (!textBeforeCursor.startsWith("/")) { + return fallbackProvider.getSuggestions(lines, cursorLine, cursorCol); + } -const deleteWordLeft = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - let idx = state.cursor; - while (idx > 0 && isWhitespace(graphemes[idx - 1])) { - idx -= 1; - } - while (idx > 0 && !isWhitespace(graphemes[idx - 1])) { - idx -= 1; - } - graphemes.splice(idx, state.cursor - idx); - state.cursor = idx; - state.buffer = graphemes.join(""); -}; + const aliasArgumentSuggestions = getAliasArgumentSuggestions( + textBeforeCursor, + commandSuggestionsByName + ); + if (aliasArgumentSuggestions) { + return aliasArgumentSuggestions; + } -const deleteWordRight = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - let idx = state.cursor; - while (idx < graphemes.length && isWhitespace(graphemes[idx])) { - idx += 1; - } - while (idx < graphemes.length && !isWhitespace(graphemes[idx])) { - idx += 1; - } - graphemes.splice(state.cursor, idx - state.cursor); - state.buffer = graphemes.join(""); -}; + const defaultSuggestions = fallbackProvider.getSuggestions( + lines, + cursorLine, + cursorCol + ); -const moveLineStart = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - state.cursor = findLineStart(graphemes, state.cursor); -}; + if (textBeforeCursor.includes(" ")) { + return defaultSuggestions; + } -const moveLineEnd = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - state.cursor = findLineEnd(graphemes, state.cursor); -}; + const query = textBeforeCursor.slice(1).toLowerCase(); + if (query.length === 0) { + return defaultSuggestions; + } -const deleteToLineStart = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - const lineStart = findLineStart(graphemes, state.cursor); - graphemes.splice(lineStart, state.cursor - lineStart); - state.cursor = lineStart; - state.buffer = graphemes.join(""); -}; + const aliasMatches = getAliasMatches( + query, + aliasToCanonicalName, + commandSuggestionsByName + ); -const deleteToLineEnd = (state: InputState): void => { - const graphemes = splitGraphemes(state.buffer); - const lineEnd = findLineEnd(graphemes, state.cursor); - graphemes.splice(state.cursor, lineEnd - state.cursor); - state.buffer = graphemes.join(""); + if (aliasMatches.length === 0) { + return defaultSuggestions; + } + + return { + items: mergeAutocompleteItems(aliasMatches, defaultSuggestions?.items), + prefix: textBeforeCursor, + }; + }, + applyCompletion: (lines, cursorLine, cursorCol, item, prefix) => + fallbackProvider.applyCompletion( + lines, + cursorLine, + cursorCol, + item, + prefix + ), + }; }; -const createEscapeToken = ( - action: EscapeAction, - length: number -): InputToken => ({ - type: "escape", - action, - length, -}); +interface CliUi { + chatContainer: Container; + clearStatus: () => void; + dispose: () => void; + editor: Editor; + markdownTheme: MarkdownTheme; + requestExit: () => void; + showLoader: (message: string) => void; + showModelSelector: ( + models: ModelInfo[], + currentModelId: string, + currentProvider: ProviderType, + initialFilter?: string + ) => Promise; + showThinkSelector: (currentEnabled: boolean) => Promise<"on" | "off" | null>; + showToolFallbackSelector: ( + currentMode: ToolFallbackMode + ) => Promise; + tui: TUI; + updateHeader: () => void; + waitForInput: () => Promise; +} -const parsePasteToken = (rawBuffer: string): InputToken | null => { - if (rawBuffer.startsWith(PASTE_START)) { - return { type: "paste-start", length: PASTE_START.length }; - } - if (rawBuffer.startsWith(PASTE_END)) { - return { type: "paste-end", length: PASTE_END.length }; - } - return null; -}; +const createCliUi = (skills: SkillInfo[]): CliUi => { + const markdownTheme = createMarkdownTheme(); + const tui = new TUI(new ProcessTerminal()); + tui.setClearOnShrink(true); + const headerContainer = new Container(); + const chatContainer = new Container(); + const statusContainer = new Container(); + const editorContainer = new Container(); + + const title = new Text("", 1, 0); + const help = new Text( + style( + ANSI_DIM, + "Enter to submit, Shift+Enter for newline, /help for commands, Ctrl+C clears input, Ctrl+C again exits" + ), + 1, + 0 + ); -const parseCsiNumbers = (params: string): number[] => { - if (!params) { - return []; - } - return params - .split(";") - .map((value) => Number.parseInt(value.replace("?", ""), 10)) - .filter((value) => Number.isFinite(value)); -}; + const updateHeader = (): void => { + const sessionId = getSessionId(); + const provider = agentManager.getProvider(); + const model = agentManager.getModelId(); + title.setText( + `${style(`${ANSI_BOLD}${ANSI_BRIGHT_CYAN}`, "Code Editing Agent")} ${style( + ANSI_DIM, + `${provider}/${model}` + )}\n${style(ANSI_DIM, `Session: ${sessionId}`)}` + ); + tui.requestRender(); + }; + + headerContainer.addChild(new Spacer(1)); + headerContainer.addChild(title); + headerContainer.addChild(help); + headerContainer.addChild(new Spacer(1)); -const parseCsiAction = ( - final: string, - numbers: number[] -): EscapeAction | null => { - const hasModifier = (value: number): boolean => numbers.includes(value); - const actionForDirection = (direction: "left" | "right"): EscapeAction => { - if (hasModifier(9)) { - return direction === "left" ? "line-start" : "line-end"; + const editorTheme = createEditorTheme(); + const editor = new Editor(tui, editorTheme, { + paddingX: 1, + autocompleteMaxVisible: 8, + }); + editor.setAutocompleteProvider(createAliasAwareAutocompleteProvider(skills)); + + editorContainer.addChild(editor); + + tui.addChild(headerContainer); + tui.addChild(chatContainer); + tui.addChild(statusContainer); + tui.addChild(editorContainer); + tui.setFocus(editor); + + let loader: Loader | null = null; + let inputResolver: ((value: string | null) => void) | null = null; + let pendingExitConfirmation = false; + let lastCtrlCPressAt = 0; + let activeModalCancel: (() => void) | null = null; + + const clearStatus = (): void => { + if (loader) { + loader.stop(); + statusContainer.removeChild(loader); + loader = null; } - if (hasModifier(3) || hasModifier(5)) { - return direction === "left" ? "word-left" : "word-right"; + statusContainer.clear(); + tui.requestRender(); + }; + + const showLoader = (message: string): void => { + clearStatus(); + loader = new Loader( + tui, + (text: string) => style(ANSI_CYAN, text), + (text: string) => style(ANSI_DIM, text), + message + ); + statusContainer.addChild(loader); + loader.start(); + tui.requestRender(); + }; + + const clearPromptInput = (): void => { + editor.setText(""); + tui.setFocus(editor); + tui.requestRender(); + }; + + const clearPendingExitConfirmation = (): void => { + pendingExitConfirmation = false; + }; + + const setActiveModalCancel = (cancel: (() => void) | null): void => { + activeModalCancel = cancel; + }; + + const dismissActiveModal = (): void => { + if (!activeModalCancel) { + return; } - return direction; + + const cancel = activeModalCancel; + activeModalCancel = null; + cancel(); }; - if (final === "D") { - return actionForDirection("left"); - } - if (final === "C") { - return actionForDirection("right"); - } - if (final === "A") { - return "up"; - } - if (final === "B") { - return "down"; - } - if (final === "H") { - return "home"; - } - if (final === "F") { - return "end"; - } - if (final === "~") { - const code = numbers[0]; - if (code === 3) { - return "delete"; + const shouldClearPendingExitConfirmation = (data: string): boolean => { + if (!pendingExitConfirmation) { + return false; } - if (code === 1 || code === 7) { - return "home"; + + if (isCtrlCInput(data)) { + return false; } - if (code === 4 || code === 8) { - return "end"; + + if (isKeyRelease(data) || isKeyRepeat(data)) { + return false; } - } - return null; -}; -const parseCsiToken = (rawBuffer: string): InputToken | null => { - if (rawBuffer.length < 3) { - return null; - } + return true; + }; - let params = ""; - let idx = 2; - - while (idx < rawBuffer.length) { - const code = rawBuffer.charCodeAt(idx); - if (code >= 0x40 && code <= 0x7e) { - const final = rawBuffer[idx]; - const numbers = parseCsiNumbers(params); - const action = parseCsiAction(final, numbers); - const length = idx + 1; - return action - ? createEscapeToken(action, length) - : { type: "ignore", length }; + const isCtrlCInput = (data: string): boolean => { + return data === CTRL_C_ETX || matchesKey(data, Key.ctrl("c")); + }; + + const handleCtrlCPress = (): void => { + const now = Date.now(); + + // Double press within window: force exit immediately (upstream pattern) + if (now - lastCtrlCPressAt < CTRL_C_EXIT_WINDOW_MS) { + lastCtrlCPressAt = 0; + dismissActiveModal(); + exitWithCleanup(0); + return; } - params += rawBuffer[idx]; - idx += 1; - } + lastCtrlCPressAt = now; + // First press: try to cancel active stream + const canceled = cancelActiveStream(); + if (canceled) { + pendingExitConfirmation = true; + clearStatus(); + return; + } - return null; -}; + // First press, no active stream: clear prompt + pendingExitConfirmation = true; + dismissActiveModal(); + clearPromptInput(); + }; -const parseSs3Token = (rawBuffer: string): InputToken | null => { - if (rawBuffer.length < 3) { - return null; - } - const third = rawBuffer[2]; - if (third === "D") { - return createEscapeToken("left", 3); - } - if (third === "C") { - return createEscapeToken("right", 3); - } - if (third === "H") { - return createEscapeToken("home", 3); - } - if (third === "F") { - return createEscapeToken("end", 3); - } - return { type: "ignore", length: 3 }; -}; + const showThinkSelector = async ( + currentEnabled: boolean + ): Promise<"on" | "off" | null> => { + clearStatus(); -const parseAltToken = (rawBuffer: string): InputToken | null => { - if (rawBuffer.length < 2) { - return null; - } - const next = rawBuffer[1]; - if (next === "b") { - return createEscapeToken("word-left", 2); - } - if (next === "f") { - return createEscapeToken("word-right", 2); - } - if (next === "d") { - return createEscapeToken("delete-word-right", 2); - } - if (next === "\u007f" || next === "\b") { - return createEscapeToken("delete-word-left", 2); - } - return null; -}; - -const readEscapeToken = (rawBuffer: string): InputToken | null => { - const pasteToken = parsePasteToken(rawBuffer); - if (pasteToken) { - return pasteToken; - } + const selectorContainer = new Container(); + selectorContainer.addChild( + new Text(style(ANSI_DIM, "Select reasoning execution"), 1, 0) + ); + selectorContainer.addChild(new Spacer(1)); - if (rawBuffer.length < 2) { - return null; - } + const selectList = new SelectList( + [ + { + value: "on", + label: "on", + description: "Enable model reasoning", + }, + { + value: "off", + label: "off", + description: "Disable model reasoning", + }, + ], + 2, + editorTheme.selectList + ); + selectList.setSelectedIndex(currentEnabled ? 0 : 1); - const second = rawBuffer[1]; - if (second === "[") { - return parseCsiToken(rawBuffer); - } - if (second === "O") { - return parseSs3Token(rawBuffer); - } + selectorContainer.addChild(selectList); + statusContainer.addChild(selectorContainer); + tui.requestRender(); - const altToken = parseAltToken(rawBuffer); - if (altToken) { - return altToken; - } + return await new Promise((resolve) => { + let removeSelectorInputListener: () => void = () => undefined; + let done = false; - return { type: "ignore", length: 1 }; -}; + const cleanup = (): void => { + removeSelectorInputListener(); + statusContainer.removeChild(selectorContainer); + tui.requestRender(); + }; -const readEscapeTokenFromSequence = (sequence: string): InputToken | null => { - const token = readEscapeToken(sequence); - if (!token) { - return null; - } - if (token.type === "ignore") { - return { type: "ignore", length: sequence.length }; - } - if (token.type === "paste-start" || token.type === "paste-end") { - return { type: "ignore", length: sequence.length }; - } - return token; -}; + const finish = (value: "on" | "off" | null): void => { + if (done) { + return; + } + done = true; + setActiveModalCancel(null); + cleanup(); + resolve(value); + }; -/** - * Get command suggestions based on the current input buffer. - * Returns an array of Suggestion objects with value and description. - * Also includes available skills. - */ -const getCommandSuggestions = (buffer: string): Suggestion[] => { - if (!buffer.startsWith("/")) { - return []; - } + setActiveModalCancel(() => { + finish(null); + }); - const commandMap = getCommands(); + selectList.onSelect = (item) => { + finish(item.value === "off" ? "off" : "on"); + }; + selectList.onCancel = () => { + finish(null); + }; - // Check if buffer contains a space (command + argument) - const spaceIndex = buffer.indexOf(" "); + removeSelectorInputListener = tui.addInputListener((data) => { + if (isCtrlCInput(data)) { + handleCtrlCPress(); + finish(null); + return { consume: true }; + } - if (spaceIndex === -1) { - // No space: suggest command names and skills - const suggestions: Suggestion[] = []; + if (shouldClearPendingExitConfirmation(data)) { + clearPendingExitConfirmation(); + } - // Add built-in commands - for (const [name, cmd] of commandMap) { - suggestions.push({ - value: `/${name}`, - description: cmd.description, + selectList.handleInput(data); + tui.requestRender(); + return { consume: true }; }); - } + }); + }; - // Add skills (avoid duplicates) - const commandNames = new Set(commandMap.keys()); - for (const skill of cachedSkills) { - if (!commandNames.has(skill.id)) { - suggestions.push({ - value: `/${skill.id}`, - description: skill.description, - }); - } - } + const showToolFallbackSelector = async ( + currentMode: ToolFallbackMode + ): Promise => { + clearStatus(); - // If the buffer is exactly "/", return all - if (buffer === "/") { - return suggestions.sort((a, b) => a.value.localeCompare(b.value)); - } + const selectorContainer = new Container(); + selectorContainer.addChild( + new Text(style(ANSI_DIM, "Select tool fallback mode"), 1, 0) + ); + selectorContainer.addChild(new Spacer(1)); - // Filter by prefix match - const matches = suggestions.filter((s) => - s.value.toLowerCase().startsWith(buffer.toLowerCase()) + const selectList = new SelectList( + [ + { + value: "disable", + label: buildCurrentIndicatorLabel( + "disable", + currentMode === "disable" + ), + description: "Use native tool support only", + }, + { + value: "morphxml", + label: buildCurrentIndicatorLabel( + "morphxml", + currentMode === "morphxml" + ), + description: "XML tags per tool (MorphXML protocol)", + }, + { + value: "hermes", + label: buildCurrentIndicatorLabel("hermes", currentMode === "hermes"), + description: "Hermes JSON-in-XML tool_call format", + }, + { + value: "qwen3coder", + label: buildCurrentIndicatorLabel( + "qwen3coder", + currentMode === "qwen3coder" + ), + description: "Qwen3Coder function-tag tool_call format", + }, + ], + 4, + editorTheme.selectList ); + const currentModeIndex = TOOL_FALLBACK_MODES.indexOf(currentMode); + if (currentModeIndex >= 0) { + selectList.setSelectedIndex(currentModeIndex); + } - return matches.sort((a, b) => a.value.localeCompare(b.value)); - } + selectorContainer.addChild(selectList); + statusContainer.addChild(selectorContainer); + tui.requestRender(); - // Space found: suggest arguments - const commandName = buffer.slice(1, spaceIndex); - const argPart = buffer.slice(spaceIndex + 1); + return await new Promise((resolve) => { + let removeSelectorInputListener: () => void = () => undefined; + let done = false; - const command = commandMap.get(commandName); - if (!command?.argumentSuggestions) { - return []; - } + const cleanup = (): void => { + removeSelectorInputListener(); + statusContainer.removeChild(selectorContainer); + tui.requestRender(); + }; + + const finish = (value: ToolFallbackMode | null): void => { + if (done) { + return; + } + done = true; + setActiveModalCancel(null); + cleanup(); + resolve(value); + }; + + setActiveModalCancel(() => { + finish(null); + }); + + selectList.onSelect = (item) => { + const parsedMode = parseToolFallbackMode(item.value); + finish(parsedMode); + }; + selectList.onCancel = () => { + finish(null); + }; + + removeSelectorInputListener = tui.addInputListener((data) => { + if (isCtrlCInput(data)) { + handleCtrlCPress(); + finish(null); + return { consume: true }; + } - // If no argument typed yet, return all suggestions with full command prefix - if (argPart === "") { - return command.argumentSuggestions.map((arg) => - createArgumentSuggestion(commandName, arg) + if (shouldClearPendingExitConfirmation(data)) { + clearPendingExitConfirmation(); + } + + selectList.handleInput(data); + tui.requestRender(); + return { consume: true }; + }); + }); + }; + + const showModelSelector = async ( + models: ModelInfo[], + currentModelId: string, + currentProvider: ProviderType, + initialFilter = "" + ): Promise => { + clearStatus(); + + const selectorContainer = new Container(); + + const searchInput = new Input(); + searchInput.focused = true; + searchInput.setValue(initialFilter); + selectorContainer.addChild(searchInput); + selectorContainer.addChild(new Spacer(1)); + + const modelMap = new Map(); + const items = models.map((model) => { + const key = `${model.provider}:${model.id}`; + modelMap.set(key, model); + const providerLabel = + model.provider === "anthropic" ? "Anthropic" : "FriendliAI"; + const isCurrent = + model.id === currentModelId && model.provider === currentProvider; + + return { + value: key, + label: buildModelSelectorLabel(model.id, isCurrent), + description: buildModelSelectorDescription(model.name, providerLabel), + }; + }); + + const selectList = new SelectList(items, 10, editorTheme.selectList); + const currentIndex = items.findIndex( + (item) => item.value === `${currentProvider}:${currentModelId}` ); - } + if (currentIndex >= 0) { + selectList.setSelectedIndex(currentIndex); + } - // Check if argPart exactly matches one of the suggestions - const exactMatch = command.argumentSuggestions.some( - (arg) => arg.toLowerCase() === argPart.toLowerCase() - ); + if (initialFilter.length > 0) { + selectList.setFilter(initialFilter); + } - // If exact match, return all suggestions for cycling - if (exactMatch) { - return command.argumentSuggestions.map((arg) => - createArgumentSuggestion(commandName, arg) + selectorContainer.addChild(selectList); + selectorContainer.addChild(new Spacer(1)); + selectorContainer.addChild( + new Text( + style(ANSI_DIM, "Type to filter, Enter to select, Esc to cancel"), + 1, + 0 + ) ); - } + statusContainer.addChild(selectorContainer); + tui.requestRender(); + + return await new Promise((resolve) => { + let removeSelectorInputListener: () => void = () => undefined; + let done = false; + + const cleanup = (): void => { + removeSelectorInputListener(); + statusContainer.removeChild(selectorContainer); + searchInput.focused = false; + tui.requestRender(); + }; - // Filter argument suggestions that start with the typed argument - const matches = command.argumentSuggestions.filter((arg) => - arg.toLowerCase().startsWith(argPart.toLowerCase()) - ); + const finish = (value: ModelInfo | null): void => { + if (done) { + return; + } + done = true; + setActiveModalCancel(null); + cleanup(); + resolve(value); + }; - return matches.map((arg) => createArgumentSuggestion(commandName, arg)); -}; + setActiveModalCancel(() => { + finish(null); + }); -/** - * Update suggestions based on the current buffer. - */ -const updateSuggestions = (state: InputState): void => { - state.suggestions = getCommandSuggestions(state.buffer); - state.suggestionIndex = 0; -}; + const selectCurrent = (): void => { + const selectedItem = selectList.getSelectedItem(); + if (!selectedItem) { + return; + } + finish(modelMap.get(selectedItem.value) ?? null); + }; -/** - * Collects user input with support for multi-line pastes using bracketed paste mode. - * - When text is pasted, newlines within the paste are preserved in the buffer - * - Input is only submitted when Enter is pressed outside of a paste operation - * - Supports basic line editing (backspace, Ctrl+C, Ctrl+D) - */ -const collectMultilineInput = ( - rl: ReadlineInterface, - prompt: string -): Promise => { - // Non-TTY fallback: read input using readline events for piped input - if (!process.stdin.isTTY) { - return new Promise((resolve) => { - process.stdout.write(prompt); - let allInput = ""; - const onLine = (line: string) => { - allInput += `${line}\n`; + selectList.onSelect = (item) => { + finish(modelMap.get(item.value) ?? null); + }; + selectList.onCancel = () => { + finish(null); + }; + searchInput.onSubmit = () => { + selectCurrent(); }; - const onClose = () => { - rl.removeListener("line", onLine); - rl.removeListener("close", onClose); - resolve(allInput.length > 0 ? allInput.trim() : null); + searchInput.onEscape = () => { + finish(null); }; - rl.on("line", onLine); - rl.on("close", onClose); + + removeSelectorInputListener = tui.addInputListener((data) => { + if (isCtrlCInput(data)) { + handleCtrlCPress(); + finish(null); + return { consume: true }; + } + + if (shouldClearPendingExitConfirmation(data)) { + clearPendingExitConfirmation(); + } + + if ( + matchesKey(data, Key.up) || + matchesKey(data, Key.down) || + matchesKey(data, Key.enter) || + matchesKey(data, Key.escape) + ) { + selectList.handleInput(data); + tui.requestRender(); + return { consume: true }; + } + + searchInput.handleInput(data); + selectList.setFilter(searchInput.getValue()); + tui.requestRender(); + return { consume: true }; + }); }); - } + }; - return new Promise((resolve) => { - const state: InputState = { - buffer: "", - cursor: 0, - suggestions: [], - suggestionIndex: 0, - lastSuggestionRows: 0, - lastInputRows: 1, - lastCursorRow: 0, - historyIndex: -1, - originalBuffer: "", - }; - const utf8Decoder = new TextDecoder("utf-8"); - const promptPlain = stripAnsi(prompt); - const stdinBuffer = new StdinBuffer(); - - // Store and remove existing stdin listeners to prevent double processing - const existingListeners = process.stdin.listeners("data") as (( - chunk: Buffer - ) => void)[]; - for (const listener of existingListeners) { - process.stdin.removeListener("data", listener); + const requestExit = (): void => { + pendingExitConfirmation = false; + shouldExit = true; + lastCtrlCPressAt = 0; + dismissActiveModal(); + if (inputResolver) { + const resolve = inputResolver; + inputResolver = null; + resolve(null); + } else { + exitWithCleanup(0); } + }; - // Pause readline - rl.pause(); - - const enableRawMode = () => { - if (process.stdin.isTTY) { - process.stdin.setRawMode(true); - process.stdout.write(ENABLE_BRACKETED_PASTE); - } - process.stdin.resume(); - }; + const onSigInt = () => { + handleCtrlCPress(); + }; - const disableRawMode = () => { - if (process.stdin.isTTY) { - process.stdout.write(DISABLE_BRACKETED_PASTE); - process.stdin.setRawMode(false); - } - }; + const onTerminalResize = () => { + tui.requestRender(true); + }; - const cleanup = () => { - process.stdin.removeListener("data", onData); - disableRawMode(); - // Restore previous stdin listeners - for (const listener of existingListeners) { - process.stdin.on("data", listener); - } - }; + const removeInputListener = tui.addInputListener((data) => { + if (isCtrlCInput(data)) { + handleCtrlCPress(); + return { consume: true }; + } - const finalize = (result: string | null) => { - cleanup(); - rl.resume(); - // Clear inline suggestion and suggestion list, but keep the actual input - // ANSI_CLEAR_TO_END clears from cursor to end of screen (removes inline hint + suggestion list) - process.stdout.write(ANSI_CLEAR_TO_END); - process.stdout.write("\n"); - resolve(result); - }; + if (shouldClearPendingExitConfirmation(data)) { + clearPendingExitConfirmation(); + } - const render = () => { - renderInput(state, prompt, promptPlain); - }; + return undefined; + }); - const applyAndRender = (action: () => void): InputAction | null => { - action(); - updateSuggestions(state); - render(); - return null; - }; + process.on("SIGINT", onSigInt); + process.stdout.on("resize", onTerminalResize); - const navigateSuggestionsUp = (): void => { - state.suggestionIndex = - state.suggestionIndex > 0 - ? state.suggestionIndex - 1 - : state.suggestions.length - 1; - }; + editor.onSubmit = (text: string) => { + if (!inputResolver) { + return; + } - const navigateHistoryUp = (): void => { - if (commandHistory.length === 0) { - return; - } + pendingExitConfirmation = false; + lastCtrlCPressAt = 0; + const resolve = inputResolver; + inputResolver = null; + resolve(text); + }; - if (state.historyIndex === -1) { - // Start browsing history - save current input - state.originalBuffer = state.buffer; - state.historyIndex = commandHistory.length - 1; - } else if (state.historyIndex > 0) { - state.historyIndex--; - } + const waitForInput = (): Promise => { + return new Promise((resolve) => { + inputResolver = resolve; + tui.setFocus(editor); + tui.requestRender(); + }); + }; - // Load history entry - if (state.historyIndex >= 0) { - state.buffer = commandHistory[state.historyIndex]; - state.cursor = splitGraphemes(state.buffer).length; - } - }; + const dispose = (): void => { + clearStatus(); - const navigateSuggestionsDown = (): void => { - state.suggestionIndex = - (state.suggestionIndex + 1) % state.suggestions.length; - }; + if (inputResolver) { + const resolve = inputResolver; + inputResolver = null; + resolve(null); + } - const navigateHistoryDown = (): void => { - if (state.historyIndex < commandHistory.length - 1) { - state.historyIndex++; - state.buffer = commandHistory[state.historyIndex]; - } else { - // Reached end of history - restore original input - state.historyIndex = -1; - state.buffer = state.originalBuffer; - } - state.cursor = splitGraphemes(state.buffer).length; - }; + removeInputListener(); + process.off("SIGINT", onSigInt); + process.stdout.off("resize", onTerminalResize); + tui.stop(); + }; - const applyEscapeAction = (action: EscapeAction): void => { - switch (action) { - case "left": - moveCursorLeft(state); - break; - case "right": - moveCursorRight(state); - break; - case "up": - if (state.suggestions.length > 0) { - navigateSuggestionsUp(); - } else { - navigateHistoryUp(); - } - break; - case "down": - if (state.suggestions.length > 0) { - navigateSuggestionsDown(); - } else if (state.historyIndex !== -1) { - navigateHistoryDown(); - } - break; - case "word-left": - moveWordLeft(state); - break; - case "word-right": - moveWordRight(state); - break; - case "delete-word-left": - deleteWordLeft(state); - break; - case "delete-word-right": - deleteWordRight(state); - break; - case "delete": - deleteForward(state); - break; - case "home": - case "line-start": - moveLineStart(state); - break; - case "end": - case "line-end": - moveLineEnd(state); - break; - default: - break; - } - render(); - }; + updateHeader(); + tui.start(); + + return { + tui, + editor, + chatContainer, + markdownTheme, + updateHeader, + waitForInput, + requestExit, + showLoader, + showModelSelector, + showToolFallbackSelector, + showThinkSelector, + clearStatus, + dispose, + }; +}; - const handlePasteChunk = (chunk: string): void => { - if (chunk.length === 0) { - return; - } - insertText(state, normalizeLineEndings(chunk)); - render(); - }; +registerCommand( + createRenderCommand(async () => ({ + model: agentManager.getModelId(), + modelType: agentManager.getModelType(), + instructions: await agentManager.getInstructions(), + tools: agentManager.getTools(), + messages: messageHistory.toModelMessages(), + thinkingEnabled: agentManager.isThinkingEnabled(), + toolFallbackMode: agentManager.getToolFallbackMode(), + })) +); +registerCommand(createModelCommand()); +registerCommand(createClearCommand()); +registerCommand(createThinkCommand()); +registerCommand(createToolFallbackCommand()); - const processToken = (token: InputToken): InputAction | null => { - if (token.type === "escape") { - applyEscapeAction(token.action); - return null; - } - if (token.type === "ignore") { - return null; - } - if (token.type === "text") { - return handleTextInput(token.value); - } - return null; - }; +const parseProviderArg = ( + providerArg: string | undefined +): ProviderType | null => { + if (providerArg === "anthropic" || providerArg === "friendli") { + return providerArg; + } - const handleTabCompletion = (): void => { - if (state.suggestions.length === 0) { - return; - } + return null; +}; - const currentMatchIndex = state.suggestions.findIndex( - (s) => s.value === state.buffer - ); - const isExactMatch = currentMatchIndex !== -1; - const hasMultipleSuggestions = state.suggestions.length > 1; - const cursorAtEnd = state.cursor === splitGraphemes(state.buffer).length; - - if (isExactMatch && hasMultipleSuggestions && cursorAtEnd) { - // Cycle to the next suggestion - state.suggestionIndex = - (currentMatchIndex + 1) % state.suggestions.length; - const nextSuggestion = state.suggestions[state.suggestionIndex]; - state.buffer = nextSuggestion.value; - state.cursor = splitGraphemes(nextSuggestion.value).length; - updateSuggestions(state); - render(); - } else if (state.suggestionIndex < state.suggestions.length) { - // Complete with the current suggestion - const suggestion = state.suggestions[state.suggestionIndex]; - state.buffer = suggestion.value; - state.cursor = splitGraphemes(suggestion.value).length; - updateSuggestions(state); - render(); - } +const parseToolFallbackCliOption = ( + args: string[], + index: number +): { consumedArgs: number; mode: ToolFallbackMode } | null => { + const arg = args[index]; + + if (arg === "--tool-fallback-mode") { + const candidate = args[index + 1]; + if (!candidate || candidate.startsWith("--")) { + return { + consumedArgs: 0, + mode: DEFAULT_TOOL_FALLBACK_MODE, + }; + } + const parsedMode = parseToolFallbackMode(candidate); + return { + consumedArgs: 1, + mode: parsedMode ?? DEFAULT_TOOL_FALLBACK_MODE, }; + } - const controlHandlers = new Map InputAction | null>([ - [CTRL_C, () => "cancel"], - [ - CTRL_D, - () => - state.buffer.length === 0 - ? "cancel" - : applyAndRender(() => deleteForward(state)), - ], - [CR, () => "submit"], - [LF, () => "submit"], - [CTRL_A, () => applyAndRender(() => moveLineStart(state))], - [CTRL_E, () => applyAndRender(() => moveLineEnd(state))], - [CTRL_B, () => applyAndRender(() => moveCursorLeft(state))], - [CTRL_F, () => applyAndRender(() => moveCursorRight(state))], - [CTRL_W, () => applyAndRender(() => deleteWordLeft(state))], - [CTRL_U, () => applyAndRender(() => deleteToLineStart(state))], - [CTRL_K, () => applyAndRender(() => deleteToLineEnd(state))], - [BACKSPACE_1, () => applyAndRender(() => deleteBackward(state))], - [BACKSPACE_2, () => applyAndRender(() => deleteBackward(state))], - ]); - - const handleTextInput = (value: string): InputAction | null => { - const code = value.charCodeAt(0); - const handler = controlHandlers.get(code); - if (handler) { - return handler(); - } + if (arg !== "--tool-fallback") { + return null; + } - if (code === TAB) { - handleTabCompletion(); - return null; - } + const candidate = args[index + 1]; + if (candidate && !candidate.startsWith("--")) { + const parsedMode = parseToolFallbackMode(candidate); + return { + consumedArgs: 1, + mode: parsedMode ?? LEGACY_ENABLED_TOOL_FALLBACK_MODE, + }; + } - if (code < 32) { - return null; - } + return { + consumedArgs: 0, + mode: LEGACY_ENABLED_TOOL_FALLBACK_MODE, + }; +}; - // Cancel history browsing when typing - if (state.historyIndex !== -1) { - state.historyIndex = -1; - state.originalBuffer = ""; - } +const parseCliArgs = (): { + thinking: boolean; + toolFallbackMode: ToolFallbackMode; + model: string | null; + provider: ProviderType | null; +} => { + const args = process.argv.slice(2); + let thinking = false; + let toolFallbackMode: ToolFallbackMode = DEFAULT_TOOL_FALLBACK_MODE; + let model: string | null = null; + let provider: ProviderType | null = null; - insertText(state, normalizeLineEndings(value)); - updateSuggestions(state); - render(); - return null; - }; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === "--think") { + thinking = true; + continue; + } - const handleSequence = (sequence: string): InputAction | null => { - if (sequence.length === 0) { - return null; - } - if (sequence.startsWith("\x1b")) { - const token = readEscapeTokenFromSequence(sequence); - return token ? processToken(token) : null; - } - return handleTextInput(sequence); - }; + const toolFallbackOption = parseToolFallbackCliOption(args, i); + if (toolFallbackOption) { + toolFallbackMode = toolFallbackOption.mode; + i += toolFallbackOption.consumedArgs; + continue; + } - const onSequence = (sequence: string) => { - const action = handleSequence(sequence); - if (action === "submit") { - // If suggestions are displayed and cursor is at end, apply the selected suggestion first (like Tab) - const cursorAtEnd = - state.cursor === splitGraphemes(state.buffer).length; - if ( - state.suggestions.length > 0 && - state.suggestionIndex < state.suggestions.length && - cursorAtEnd && - state.buffer !== state.suggestions[state.suggestionIndex].value - ) { - // Apply suggestion without submitting (same as Tab behavior) - const suggestion = state.suggestions[state.suggestionIndex]; - state.buffer = suggestion.value; - state.cursor = splitGraphemes(suggestion.value).length; - updateSuggestions(state); - render(); - return; - } - finalize(state.buffer); - return; - } - if (action === "cancel") { - finalize(null); - } - }; + if (arg === "--model" && i + 1 < args.length) { + model = args[i + 1]; + i += 1; + continue; + } - const onData = (data: Buffer) => { - const decoded = utf8Decoder.decode(data, { stream: true }); - if (decoded.length === 0) { - return; - } - stdinBuffer.process(decoded); - }; + if (arg === "--provider" && i + 1 < args.length) { + provider = parseProviderArg(args[i + 1]) ?? provider; + i += 1; + } + } - enableRawMode(); - renderInput(state, prompt, promptPlain); - stdinBuffer.on("data", onSequence); - stdinBuffer.on("paste", handlePasteChunk); - process.stdin.on("data", onData); - }); + return { thinking, toolFallbackMode, model, provider }; }; const setupAgent = (): void => { - const { thinking, toolFallback, model, provider } = parseCliArgs(); + const { thinking, toolFallbackMode, model, provider } = parseCliArgs(); agentManager.setThinkingEnabled(thinking); - agentManager.setToolFallbackEnabled(toolFallback); + agentManager.setToolFallbackMode(toolFallbackMode); if (provider) { agentManager.setProvider(provider); } @@ -1470,109 +1250,380 @@ const setupAgent = (): void => { } }; -const addToHistory = (trimmed: string): void => { - if ( - trimmed.length > 0 && - (commandHistory.length === 0 || commandHistory.at(-1) !== trimmed) - ) { - commandHistory.push(trimmed); +type AgentResponseStatus = "completed" | "interrupted"; + +const processAgentResponse = async ( + ui: CliUi +): Promise => { + let manualToolLoopCount = 0; + + while (true) { + ui.showLoader(manualToolLoopCount === 0 ? "Thinking..." : "Continuing..."); + const streamAbortController = new AbortController(); + activeStreamController = streamAbortController; + streamInterruptRequested = false; + + try { + const stream = await agentManager.stream( + messageHistory.toModelMessages(), + { abortSignal: streamAbortController.signal } + ); + ui.clearStatus(); + + await renderFullStreamWithPiTui(stream.fullStream, { + ui: ui.tui, + chatContainer: ui.chatContainer, + markdownTheme: ui.markdownTheme, + showReasoning: true, + showSteps: false, + showToolResults: true, + showFiles: false, + showSources: false, + showFinishReason: env.DEBUG_SHOW_FINISH_REASON, + }); + + const [response, finishReason] = await Promise.all([ + stream.response, + stream.finishReason, + ]); + + if (streamInterruptRequested || streamAbortController.signal.aborted) { + addSystemMessage( + ui.chatContainer, + "[agent] Stream interrupted by user. Waiting for input." + ); + ui.tui.requestRender(); + return "interrupted"; + } + + messageHistory.addModelMessages(response.messages); + + if (!shouldContinueManualToolLoop(finishReason)) { + return "completed"; + } + + manualToolLoopCount += 1; + if (manualToolLoopCount >= MANUAL_TOOL_LOOP_MAX_STEPS) { + addSystemMessage( + ui.chatContainer, + `[agent] Manual tool loop safety cap reached (${MANUAL_TOOL_LOOP_MAX_STEPS}); waiting for input.` + ); + ui.tui.requestRender(); + return "completed"; + } + } catch (error) { + if (streamInterruptRequested || streamAbortController.signal.aborted) { + addSystemMessage( + ui.chatContainer, + "[agent] Stream interrupted by user. Waiting for input." + ); + ui.tui.requestRender(); + return "interrupted"; + } + + throw error; + } finally { + clearActiveStreamController(streamAbortController); + streamInterruptRequested = false; + ui.clearStatus(); + } + } +}; + +const handleAgentResponse = async (ui: CliUi): Promise => { + while (true) { + const result = await processAgentResponse(ui); + if (result === "interrupted") { + return; + } + + const incompleteTodos = await getIncompleteTodos(); + if (incompleteTodos.length === 0) { + return; + } + + const reminder = buildTodoContinuationUserMessage(incompleteTodos); + messageHistory.addUserMessage(reminder); + } +}; + +const renderCommandMessage = (ui: CliUi, message: string): void => { + addSystemMessage(ui.chatContainer, message); + ui.updateHeader(); + ui.tui.requestRender(); +}; + +const handleModelCommand = async ( + ui: CliUi, + commandInput: string, + parsed: ReturnType +): Promise => { + if (parsed?.name !== "model") { + return null; + } + + const models = getAvailableModels(); + if (models.length === 0) { + const result = await executeCommand(commandInput); + if (result?.message) { + renderCommandMessage(ui, result.message); + return true; + } + return false; + } + + const searchTerm = parsed.args[0]?.trim() ?? ""; + const exactMatch = + searchTerm.length > 0 + ? findModelBySelection(searchTerm, models) + : undefined; + + if (parsed.args.length > 0 && exactMatch) { + return null; + } + + const selectedModel = await ui.showModelSelector( + models, + agentManager.getModelId(), + agentManager.getProvider(), + searchTerm + ); + if (!selectedModel) { + return true; + } + + const selectionResult = applyModelSelection(selectedModel); + if (selectionResult.message) { + renderCommandMessage(ui, selectionResult.message); + return true; + } + + ui.updateHeader(); + ui.tui.requestRender(); + return true; +}; + +const resolveToolFallbackCommandInput = async ( + ui: CliUi, + commandInput: string, + parsed: ReturnType +): Promise => { + if (parsed?.name !== "tool-fallback" || parsed.args.length > 0) { + return commandInput; + } + + const selected = await ui.showToolFallbackSelector( + agentManager.getToolFallbackMode() + ); + if (!selected) { + return null; + } + + return `/tool-fallback ${selected}`; +}; + +const resolveThinkCommandInput = async ( + ui: CliUi, + commandInput: string, + parsed: ReturnType +): Promise => { + if (parsed?.name !== "think" || parsed.args.length > 0) { + return commandInput; } + + const selected = await ui.showThinkSelector(agentManager.isThinkingEnabled()); + if (!selected) { + return null; + } + + return `/think ${selected}`; }; -const _handleCommand = async ( - rl: ReadlineInterface, - trimmed: string -): Promise => { - const result = await executeCommand(trimmed); +const handleCommand = async (ui: CliUi, input: string): Promise => { + let commandInput = input; + const initialParsed = parseCommand(commandInput); + + const modelHandled = await handleModelCommand( + ui, + commandInput, + initialParsed + ); + if (modelHandled !== null) { + return modelHandled; + } + + const toolFallbackCommandInput = await resolveToolFallbackCommandInput( + ui, + commandInput, + initialParsed + ); + if (!toolFallbackCommandInput) { + return true; + } + commandInput = toolFallbackCommandInput; + + const thinkCommandInput = await resolveThinkCommandInput( + ui, + commandInput, + initialParsed + ); + if (!thinkCommandInput) { + return true; + } + commandInput = thinkCommandInput; + + const parsed = parseCommand(commandInput); + const resolvedCommandName = parsed + ? resolveRegisteredCommandName(parsed.name) + : null; + const isNativeCommand = + resolvedCommandName === "clear" || + resolvedCommandName === "think" || + resolvedCommandName === "tool-fallback"; + + if (!isNativeCommand) { + ui.showLoader("Running command..."); + } + + const result = await executeCommand(commandInput); + + if (!isNativeCommand) { + ui.clearStatus(); + } + + if (result?.action === "new-session") { + ui.clearStatus(); + initializeSession(); + messageHistory.clear(); + ui.chatContainer.clear(); + addNewSessionMessage(ui.chatContainer); + ui.updateHeader(); + ui.tui.requestRender(); + return true; + } + if (isSkillCommandResult(result)) { - // Inject skill content into conversation const skillMessage = `/${result.skillId}\n\n${result.skillContent}`; messageHistory.addUserMessage(skillMessage); - await handleAgentResponse(rl); + await handleAgentResponse(ui); return true; } + if (result?.message) { - console.log(result.message); + renderCommandMessage(ui, result.message); return true; } + return false; }; -const processInput = async ( - rl: ReadlineInterface, - input: string -): Promise => { +const processInput = async (ui: CliUi, input: string): Promise => { const trimmed = input.trim(); - addToHistory(trimmed); - if (shouldExitFromInput(trimmed)) { - return false; // Signal to exit - } - - if (isCommand(trimmed)) { - const shouldContinue = await _handleCommand(rl, trimmed); - return shouldContinue; + if (shouldExit || trimmed.length === 0 || trimmed.toLowerCase() === "exit") { + return false; } - messageHistory.addUserMessage(trimmed); - await handleAgentResponse(rl); - return true; -}; + ui.editor.disableSubmit = true; + try { + if (isCommand(trimmed)) { + addUserMessage(ui.chatContainer, ui.markdownTheme, trimmed); + ui.tui.requestRender(); + return await handleCommand(ui, trimmed); + } -const cleanup = (rl: ReadlineInterface): void => { - if (env.DEBUG_TMUX_CLEANUP) { - console.error("[DEBUG] Performing cleanup..."); - } - process.off("SIGINT", handleGracefulShutdown); - rlInstance = null; - rl.close(); - cleanupSession(); - if (env.DEBUG_TMUX_CLEANUP) { - console.error("[DEBUG] Cleanup completed."); + addUserMessage(ui.chatContainer, ui.markdownTheme, trimmed); + messageHistory.addUserMessage(trimmed); + ui.tui.requestRender(); + await handleAgentResponse(ui); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + addSystemMessage(ui.chatContainer, `Error: ${errorMessage}`); + ui.tui.requestRender(); + return true; + } finally { + ui.editor.disableSubmit = false; + ui.editor.setText(""); + ui.tui.setFocus(ui.editor); + ui.tui.requestRender(); } }; const run = async (): Promise => { - // Initialize required tools (ripgrep, tmux) await initializeTools(); - - // Load skills for autocomplete cachedSkills = await loadAllSkills(); + setSpinnerOutputEnabled(false); - const sessionId = initializeSession(); - console.log(colorize("dim", `Session: ${sessionId}\n`)); - + initializeSession(); setupAgent(); - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rlInstance = rl; - process.on("SIGINT", handleGracefulShutdown); + const ui = createCliUi(cachedSkills); try { while (!shouldExit) { - const input = await collectMultilineInput( - rl, - `${colorize("blue", "You")}: ` - ); - + const input = await ui.waitForInput(); if (input === null) { break; } - await processInput(rl, input); + const shouldContinue = await processInput(ui, input); + if (!shouldContinue) { + break; + } } - } catch (error) { - console.error("Error:", error); - throw error; } finally { - cleanup(rl); + ui.dispose(); + cleanupTmuxSession(); + setSpinnerOutputEnabled(true); } }; +let tmuxCleanupExecuted = false; + +const cleanupTmuxSession = (): void => { + if (tmuxCleanupExecuted) { + return; + } + tmuxCleanupExecuted = true; + + if (env.TMUX_CLEANUP_SESSION) { + cleanupSession(); + } +}; + +const exitWithCleanup = (code: number): never => { + cleanupTmuxSession(); + process.exit(code); +}; + +process.once("exit", () => { + cleanupTmuxSession(); +}); + +process.once("SIGTERM", () => { + exitWithCleanup(143); +}); + +process.once("SIGHUP", () => { + exitWithCleanup(129); +}); + +process.once("SIGQUIT", () => { + exitWithCleanup(131); +}); + +process.once("uncaughtException", (error: unknown) => { + console.error("Fatal error:", error); + exitWithCleanup(1); +}); + +process.once("unhandledRejection", (reason: unknown) => { + console.error("Unhandled rejection:", reason); + exitWithCleanup(1); +}); + run().catch((error: unknown) => { - throw error instanceof Error ? error : new Error("Failed to run stream."); + console.error("Fatal error:", error); + exitWithCleanup(1); }); diff --git a/src/entrypoints/headless.ts b/src/entrypoints/headless.ts index fba7f29..66a37a4 100644 --- a/src/entrypoints/headless.ts +++ b/src/entrypoints/headless.ts @@ -4,10 +4,20 @@ import { agentManager, DEFAULT_MODEL_ID } from "../agent"; import { MessageHistory } from "../context/message-history"; import { setSessionId } from "../context/session"; import { env } from "../env"; +import { + MANUAL_TOOL_LOOP_MAX_STEPS, + shouldContinueManualToolLoop, +} from "../interaction/tool-loop-control"; import { buildTodoContinuationUserMessage, getIncompleteTodos, } from "../middleware/todo-continuation"; +import { + DEFAULT_TOOL_FALLBACK_MODE, + LEGACY_ENABLED_TOOL_FALLBACK_MODE, + parseToolFallbackMode, + type ToolFallbackMode, +} from "../tool-fallback-mode"; import { cleanupSession } from "../tools/execute/shared-tmux-session"; import { initializeTools } from "../utils/tools-manager"; @@ -59,51 +69,140 @@ type TrajectoryEvent = const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; -process.on("SIGINT", () => { - process.exit(0); +let tmuxCleanupExecuted = false; + +const cleanupTmuxSession = (): void => { + if (tmuxCleanupExecuted) { + return; + } + tmuxCleanupExecuted = true; + + if (env.TMUX_CLEANUP_SESSION) { + cleanupSession(); + } +}; + +const exitWithCleanup = (code: number): never => { + cleanupTmuxSession(); + process.exit(code); +}; + +process.once("exit", () => { + cleanupTmuxSession(); +}); + +process.once("SIGINT", () => { + exitWithCleanup(0); +}); + +process.once("SIGTERM", () => { + exitWithCleanup(143); +}); + +process.once("SIGHUP", () => { + exitWithCleanup(129); +}); + +process.once("SIGQUIT", () => { + exitWithCleanup(131); +}); + +process.once("uncaughtException", (error: unknown) => { + console.error("Fatal error:", error); + exitWithCleanup(1); +}); + +process.once("unhandledRejection", (reason: unknown) => { + console.error("Unhandled rejection:", reason); + exitWithCleanup(1); }); const startTime = Date.now(); -const TODO_CONTINUATION_MAX_LOOPS = 5; const emitEvent = (event: TrajectoryEvent): void => { console.log(JSON.stringify(event)); }; +const parseToolFallbackCliOption = ( + args: string[], + index: number +): { consumedArgs: number; mode: ToolFallbackMode } | null => { + const arg = args[index]; + + if (arg === "--tool-fallback-mode") { + const candidate = args[index + 1]; + if (!candidate || candidate.startsWith("--")) { + return { + consumedArgs: 0, + mode: DEFAULT_TOOL_FALLBACK_MODE, + }; + } + const parsedMode = parseToolFallbackMode(candidate); + return { + consumedArgs: 1, + mode: parsedMode ?? DEFAULT_TOOL_FALLBACK_MODE, + }; + } + + if (arg !== "--tool-fallback") { + return null; + } + + const candidate = args[index + 1]; + if (candidate && !candidate.startsWith("--")) { + const parsedMode = parseToolFallbackMode(candidate); + return { + consumedArgs: 1, + mode: parsedMode ?? LEGACY_ENABLED_TOOL_FALLBACK_MODE, + }; + } + + return { + consumedArgs: 0, + mode: LEGACY_ENABLED_TOOL_FALLBACK_MODE, + }; +}; + const parseArgs = (): { prompt: string; model?: string; thinking: boolean; - toolFallback: boolean; + toolFallbackMode: ToolFallbackMode; } => { const args = process.argv.slice(2); let prompt = ""; let model: string | undefined; let thinking = false; - let toolFallback = false; + let toolFallbackMode: ToolFallbackMode = DEFAULT_TOOL_FALLBACK_MODE; for (let i = 0; i < args.length; i++) { - if (args[i] === "-p" || args[i] === "--prompt") { + const arg = args[i]; + + if (arg === "-p" || arg === "--prompt") { prompt = args[i + 1] || ""; i++; - } else if (args[i] === "-m" || args[i] === "--model") { + } else if (arg === "-m" || arg === "--model") { model = args[i + 1] || undefined; i++; - } else if (args[i] === "--think") { + } else if (arg === "--think") { thinking = true; - } else if (args[i] === "--tool-fallback") { - toolFallback = true; + } else { + const toolFallbackOption = parseToolFallbackCliOption(args, i); + if (toolFallbackOption) { + toolFallbackMode = toolFallbackOption.mode; + i += toolFallbackOption.consumedArgs; + } } } if (!prompt) { console.error( - "Usage: bun run src/entrypoints/headless.ts -p [-m ] [--think] [--tool-fallback]" + "Usage: bun run src/entrypoints/headless.ts -p [-m ] [--think] [--tool-fallback [mode]] [--tool-fallback-mode ]" ); process.exit(1); } - return { prompt, model, thinking, toolFallback }; + return { prompt, model, thinking, toolFallbackMode }; }; const extractToolOutput = ( @@ -294,74 +393,99 @@ const emitMalformedToolCallsSummary = ( const processAgentResponse = async ( messageHistory: MessageHistory ): Promise => { - const stream = await agentManager.stream(messageHistory.toModelMessages()); const modelId = agentManager.getModelId(); - - let currentText = ""; - let currentReasoning = ""; - const pendingToolCalls = new Map(); - const completedToolCallIds = new Set(); - let lastFinishReason: string | undefined; - - for await (const part of stream.fullStream) { - switch (part.type) { - case "text-delta": - currentText += part.text; - break; - case "reasoning-delta": - currentReasoning += part.text; - break; - case "tool-input-start": - handleToolInputStart(pendingToolCalls, part); - break; - case "tool-input-delta": - handleToolInputDelta(pendingToolCalls, part); - break; - case "tool-input-end": - break; - case "tool-call": - currentReasoning = emitToolCallEvent( - completedToolCallIds, - modelId, - currentReasoning, - part - ); - break; - case "tool-result": - emitToolResultEvent(part); - break; - case "tool-error": - emitToolErrorEvent(part); - break; - case "finish-step": { - const finishPart = part as { finishReason: string }; - lastFinishReason = finishPart.finishReason; - break; + let manualToolLoopCount = 0; + + while (true) { + const stream = await agentManager.stream(messageHistory.toModelMessages()); + + let currentText = ""; + let currentReasoning = ""; + const pendingToolCalls = new Map(); + const completedToolCallIds = new Set(); + let lastFinishReason: string | undefined; + + for await (const part of stream.fullStream) { + switch (part.type) { + case "text-delta": + currentText += part.text; + break; + case "reasoning-delta": + currentReasoning += part.text; + break; + case "tool-input-start": + handleToolInputStart(pendingToolCalls, part); + break; + case "tool-input-delta": + handleToolInputDelta(pendingToolCalls, part); + break; + case "tool-input-end": + break; + case "tool-call": + currentReasoning = emitToolCallEvent( + completedToolCallIds, + modelId, + currentReasoning, + part + ); + break; + case "tool-result": + emitToolResultEvent(part); + break; + case "tool-error": + emitToolErrorEvent(part); + break; + case "finish-step": { + const finishPart = part as Extract< + typeof part, + { type: "finish-step" } + >; + lastFinishReason = finishPart.finishReason; + break; + } + default: + break; } - default: - break; } - } - emitMalformedToolCallErrors(completedToolCallIds, pendingToolCalls); - emitMalformedToolCallsSummary( - completedToolCallIds, - lastFinishReason, - pendingToolCalls - ); + emitMalformedToolCallErrors(completedToolCallIds, pendingToolCalls); + emitMalformedToolCallsSummary( + completedToolCallIds, + lastFinishReason, + pendingToolCalls + ); - const response = await stream.response; - messageHistory.addModelMessages(response.messages); + const [response, finishReason] = await Promise.all([ + stream.response, + stream.finishReason, + ]); + messageHistory.addModelMessages(response.messages); - if (currentText.trim()) { - emitEvent({ - timestamp: new Date().toISOString(), - type: "assistant", - sessionId, - content: currentText, - model: modelId, - reasoning_content: currentReasoning || undefined, - }); + if (currentText.trim()) { + emitEvent({ + timestamp: new Date().toISOString(), + type: "assistant", + sessionId, + content: currentText, + model: modelId, + reasoning_content: currentReasoning || undefined, + }); + } + + if (!shouldContinueManualToolLoop(finishReason)) { + return; + } + + manualToolLoopCount += 1; + if (manualToolLoopCount >= MANUAL_TOOL_LOOP_MAX_STEPS) { + emitEvent({ + timestamp: new Date().toISOString(), + type: "error", + sessionId, + error: `Manual tool loop safety cap reached (${MANUAL_TOOL_LOOP_MAX_STEPS}).`, + }); + return; + } } }; @@ -369,14 +493,14 @@ const run = async (): Promise => { // Initialize required tools (ripgrep, tmux) await initializeTools(); - const { prompt, model, thinking, toolFallback } = parseArgs(); + const { prompt, model, thinking, toolFallbackMode } = parseArgs(); setSessionId(sessionId); agentManager.setHeadlessMode(true); agentManager.setModelId(model || DEFAULT_MODEL_ID); agentManager.setThinkingEnabled(thinking); - agentManager.setToolFallbackEnabled(toolFallback); + agentManager.setToolFallbackMode(toolFallbackMode); const messageHistory = new MessageHistory(); @@ -392,20 +516,22 @@ const run = async (): Promise => { try { await processAgentResponse(messageHistory); - let continuationCount = 0; - while (continuationCount <= TODO_CONTINUATION_MAX_LOOPS) { + const MAX_TODO_REMINDER_ITERATIONS = 20; + let todoReminderCount = 0; + + while (true) { const incompleteTodos = await getIncompleteTodos(); if (incompleteTodos.length === 0) { break; } - if (continuationCount === TODO_CONTINUATION_MAX_LOOPS) { + todoReminderCount += 1; + if (todoReminderCount > MAX_TODO_REMINDER_ITERATIONS) { emitEvent({ timestamp: new Date().toISOString(), type: "error", sessionId, - error: - "Auto-continue limit reached with incomplete todos. Awaiting new input.", + error: `Todo continuation safety cap reached (${MAX_TODO_REMINDER_ITERATIONS} reminders). Incomplete todos: ${incompleteTodos.map((t) => t.id).join(", ")}`, }); break; } @@ -418,7 +544,6 @@ const run = async (): Promise => { content: reminder, }); messageHistory.addUserMessage(reminder); - continuationCount += 1; await processAgentResponse(messageHistory); } } catch (error) { @@ -428,20 +553,15 @@ const run = async (): Promise => { sessionId, error: error instanceof Error ? error.message : String(error), }); - if (env.TMUX_CLEANUP_SESSION) { - cleanupSession(); - } - process.exit(1); + exitWithCleanup(1); } - if (env.TMUX_CLEANUP_SESSION) { - cleanupSession(); - } + cleanupTmuxSession(); const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); console.error(`[headless] Completed in ${elapsed}s`); }; run().catch((error: unknown) => { console.error("Fatal error:", error); - process.exit(1); + exitWithCleanup(1); }); diff --git a/src/env.ts b/src/env.ts index c699a1e..c5852fd 100644 --- a/src/env.ts +++ b/src/env.ts @@ -5,7 +5,6 @@ export const env = createEnv({ server: { FRIENDLI_TOKEN: z.string().min(1).optional(), ANTHROPIC_API_KEY: z.string().min(1).optional(), - EXPERIMENTAL_TRIM_TRAILING_NEWLINES: z.stringbool().default(true), DEBUG_SHOW_FINISH_REASON: z.stringbool().default(false), DEBUG_SHOW_TOOL_RESULTS: z.stringbool().default(false), DEBUG_TMUX_CLEANUP: z.stringbool().default(false), diff --git a/src/interaction/colors.ts b/src/interaction/colors.ts index 982162d..0a156e1 100644 --- a/src/interaction/colors.ts +++ b/src/interaction/colors.ts @@ -22,44 +22,3 @@ export const colors = { export function colorize(color: keyof typeof colors, text: string): string { return `${colors[color]}${text}${colors.reset}`; } - -export function printYou(): void { - process.stdout.write( - `${colors.bold}${colors.brightBlue}You${colors.reset}: ` - ); -} - -export function printAIPrefix(): void { - process.stdout.write(`${colors.bold}${colors.brightCyan}AI${colors.reset}: `); -} - -export function printReasoningPrefix(): void { - process.stdout.write(`${colors.dim}${colors.italic}${colors.gray}│ `); -} - -export function printReasoningChunk(text: string): void { - process.stdout.write(text); -} - -export function printReasoningEnd(): void { - process.stdout.write(`${colors.reset}\n`); -} - -export function printChunk(text: string): void { - process.stdout.write(text); -} - -export function printNewline(): void { - process.stdout.write("\n"); -} - -export function printTool(name: string, input: unknown): void { - const toolLabel = `${colors.bold}${colors.brightGreen}tool${colors.reset}`; - const toolName = `${colors.bold}${colors.brightYellow}${name}${colors.reset}`; - console.log(`${toolLabel} ${toolName}(${JSON.stringify(input)})`); -} - -export function printError(message: string): void { - const errorLabel = `${colors.bold}${colors.red}error${colors.reset}`; - console.error(`${errorLabel}: ${message}`); -} diff --git a/src/interaction/pi-tui-stream-renderer.test.ts b/src/interaction/pi-tui-stream-renderer.test.ts new file mode 100644 index 0000000..3ce5f07 --- /dev/null +++ b/src/interaction/pi-tui-stream-renderer.test.ts @@ -0,0 +1,839 @@ +import { describe, expect, it } from "bun:test"; +import { Container, type MarkdownTheme } from "@mariozechner/pi-tui"; +import type { TextStreamPart, ToolSet } from "ai"; +import { + type PiTuiStreamRenderOptions, + renderFullStreamWithPiTui, +} from "./pi-tui-stream-renderer"; + +type TestStreamPart = TextStreamPart; + +const LARGE_BLANK_GAP_REGEX = /\n[ \t]*\n[ \t]*\n[ \t]*\n/; + +const findLastLineIndexContaining = ( + lines: string[], + predicate: (line: string) => boolean, + beforeIndex: number +): number => { + for (let i = beforeIndex - 1; i >= 0; i -= 1) { + if (predicate(lines[i])) { + return i; + } + } + + return -1; +}; + +const markdownTheme: MarkdownTheme = { + heading: (text) => text, + link: (text) => text, + linkUrl: (text) => text, + code: (text) => text, + codeBlock: (text) => text, + codeBlockBorder: (text) => text, + quote: (text) => text, + quoteBorder: (text) => text, + hr: (text) => text, + listBullet: (text) => text, + bold: (text) => text, + italic: (text) => text, + strikethrough: (text) => text, + underline: (text) => text, +}; + +interface RenderResult { + output: string; + renderCalls: number; +} + +const renderParts = async (parts: TestStreamPart[]): Promise => { + const chatContainer = new Container(); + let renderCalls = 0; + + const options: PiTuiStreamRenderOptions = { + chatContainer, + markdownTheme, + ui: { + requestRender: () => { + renderCalls += 1; + }, + }, + showReasoning: true, + showSteps: false, + showFinishReason: false, + showToolResults: true, + showSources: false, + showFiles: false, + }; + + async function* stream(): AsyncIterable { + for (const part of parts) { + await Promise.resolve(); + yield part; + } + } + + await renderFullStreamWithPiTui(stream(), options); + const output = chatContainer.render(120).join("\n"); + + return { output, renderCalls }; +}; + +describe("renderFullStreamWithPiTui", () => { + it("streams markdown text into assistant view", async () => { + const { output, renderCalls } = await renderParts([ + { type: "text-start", id: "text_1" }, + { type: "text-delta", id: "text_1", text: "Hello" }, + { type: "text-delta", id: "text_1", text: " world" }, + { type: "text-end", id: "text_1" }, + ]); + + expect(output).toContain("Hello world"); + expect(renderCalls).toBeGreaterThan(0); + }); + + it("preserves stream order between reasoning and text blocks", async () => { + const { output } = await renderParts([ + { type: "reasoning-start", id: "reason_1" } as never, + { type: "reasoning-delta", id: "reason_1", text: "First thought" }, + { type: "reasoning-end", id: "reason_1" } as never, + { type: "text-start", id: "text_2" }, + { type: "text-delta", id: "text_2", text: "Final answer" }, + { type: "text-end", id: "text_2" }, + ]); + + const reasoningIndex = output.indexOf("First thought"); + const textIndex = output.indexOf("Final answer"); + + expect(reasoningIndex).toBeGreaterThan(-1); + expect(textIndex).toBeGreaterThan(-1); + expect(reasoningIndex).toBeLessThan(textIndex); + }); + + it("applies Pi-like muted italic styling to reasoning text", async () => { + const { output } = await renderParts([ + { type: "reasoning-start", id: "reason_2" } as never, + { type: "reasoning-delta", id: "reason_2", text: "styled reasoning" }, + { type: "reasoning-end", id: "reason_2" } as never, + ]); + + expect(output).toContain("styled reasoning"); + expect(output).toContain("\x1b[2m\x1b[3m\x1b[90m"); + }); + + it("removes leading newlines from reasoning display", async () => { + const { output } = await renderParts([ + { type: "reasoning-start", id: "reason_trim" } as never, + { + type: "reasoning-delta", + id: "reason_trim", + text: "\n\nreasoning without top blank lines", + }, + { type: "reasoning-end", id: "reason_trim" } as never, + ]); + + expect(output).toContain("reasoning without top blank lines"); + expect(output).not.toContain("\x1b[2m\x1b[3m\x1b[90m\n"); + }); + + it("avoids large gap between tool output and following reasoning", async () => { + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_gap", + toolName: "bash", + input: { + command: "pwd", + }, + }, + { + type: "tool-result", + toolCallId: "call_gap", + toolName: "bash", + input: { + command: "pwd", + }, + output: "tool output line\n\n\n", + }, + { type: "reasoning-start", id: "reason_gap" } as never, + { + type: "reasoning-delta", + id: "reason_gap", + text: "After tool output", + }, + { type: "reasoning-end", id: "reason_gap" } as never, + ]); + + const plain = output; + const start = plain.indexOf("tool output line"); + const end = plain.indexOf("After tool output"); + expect(start).toBeGreaterThan(-1); + expect(end).toBeGreaterThan(start); + + const between = plain.slice(start, end); + expect(between).not.toMatch(LARGE_BLANK_GAP_REGEX); + + const lines = plain.split("\n"); + const reasoningLineIndex = lines.findIndex((line) => + line.includes("After tool output") + ); + expect(reasoningLineIndex).toBeGreaterThan(-1); + + const outputFenceIndex = findLastLineIndexContaining( + lines, + (line) => line.trim() === "```", + reasoningLineIndex + ); + expect(outputFenceIndex).toBeGreaterThan(-1); + expect(reasoningLineIndex).toBe(outputFenceIndex + 1); + }); + + it("renders live diff preview for edit_file tool input", async () => { + const { output } = await renderParts([ + { + type: "tool-input-start", + id: "call_edit", + toolName: "edit_file", + }, + { + type: "tool-input-delta", + id: "call_edit", + delta: + '{"path":"src/demo.ts","old_str":"const value = 1;","new_str":"const value = 2;"}', + }, + { type: "tool-input-end", id: "call_edit" }, + { + type: "tool-call", + toolCallId: "call_edit", + toolName: "edit_file", + input: { + path: "src/demo.ts", + old_str: "const value = 1;", + new_str: "const value = 2;", + }, + }, + ]); + + expect(output).toContain("Live diff preview"); + expect(output).toContain("-const value = 1;"); + expect(output).toContain("+const value = 2;"); + }); + + it("renders read_file output as structured markdown", async () => { + const readOutput = [ + "OK - read file", + "path: src/demo.ts", + "bytes: 48", + "last_modified: 2026-01-19T03:33:57.520Z", + "lines: 5 (returned: 4)", + "range: L2-L5", + "truncated: true", + "", + "======== demo.ts L2-L5 ========", + " 2 | const value = 2;", + " 3 | export { value };", + " 4 | ```md", + " 5 | ![Image 1](./img.png)", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_read", + toolName: "read_file", + input: { + path: "src/demo.ts", + }, + }, + { + type: "tool-result", + toolCallId: "call_read", + toolName: "read_file", + input: { + path: "src/demo.ts", + }, + output: readOutput, + }, + ]); + + expect(output).toContain("Read src/demo.ts L2-L5"); + expect(output).toContain("2 | "); + expect(output).toContain("const value = 2;"); + expect(output).toContain("![Image 1](./img.png)"); + expect(output).toContain("... (1 more line, truncated)"); + expect(output).not.toContain("4 | ```md"); + expect(output).not.toContain("```md"); + expect(output).not.toContain("Tool read_file"); + expect(output).not.toContain("Output"); + expect(output).not.toContain("OK - read file"); + }); + + it("omits read_file content after 10 lines", async () => { + const numberedLines = Array.from({ length: 12 }, (_, index) => { + const lineNumber = index + 1; + return `${lineNumber.toString().padStart(4, " ")} | line ${lineNumber}`; + }); + + const readOutput = [ + "OK - read file", + "path: src/long.txt", + "bytes: 120", + "last_modified: 2026-02-23T01:00:00.000Z", + "lines: 12 (returned: 12)", + "range: L1-L12", + "truncated: false", + "", + "======== long.txt L1-L12 ========", + ...numberedLines, + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_long", + toolName: "read_file", + input: { + path: "src/long.txt", + }, + }, + { + type: "tool-result", + toolCallId: "call_long", + toolName: "read_file", + input: { + path: "src/long.txt", + }, + output: readOutput, + }, + ]); + + expect(output).toContain("Read src/long.txt L1-L12"); + expect(output).toContain("10 | line 10"); + expect(output).not.toContain("11 | line 11"); + expect(output).toContain("... (2 more lines)"); + }); + + it("truncates long read_file lines instead of wrapping", async () => { + const longTail = "X".repeat(180); + const readOutput = [ + "OK - read file", + "path: src/wrap.txt", + "bytes: 999", + "last_modified: 2026-02-23T01:00:00.000Z", + "lines: 1 (returned: 1)", + "range: L1-L1", + "truncated: false", + "", + "======== wrap.txt L1-L1 ========", + ` 1 | prefix ${longTail}`, + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_wrap", + toolName: "read_file", + input: { + path: "src/wrap.txt", + }, + }, + { + type: "tool-result", + toolCallId: "call_wrap", + toolName: "read_file", + input: { + path: "src/wrap.txt", + }, + output: readOutput, + }, + ]); + + expect(output).toContain("Read src/wrap.txt L1-L1"); + expect(output).toContain("1 | prefix"); + expect(output).not.toContain(longTail); + }); + + it("does not duplicate tool call blocks when input was streamed", async () => { + const { output } = await renderParts([ + { + type: "tool-input-start", + id: "call_1", + toolName: "write_file", + }, + { + type: "tool-input-delta", + id: "call_1", + delta: '{"path":"src/file.ts","content":"hello"}', + }, + { type: "tool-input-end", id: "call_1" }, + { + type: "tool-call", + toolCallId: "call_1", + toolName: "write_file", + input: { + path: "src/file.ts", + content: "hello", + }, + }, + ]); + + expect(output).toContain("call_1"); + expect((output.match(/call_1/g) ?? []).length).toBe(1); + }); + + it("supports toolCallId and inputTextDelta aliases", async () => { + const { output } = await renderParts([ + { + type: "tool-input-start", + toolCallId: "call_3", + toolName: "write_file", + } as never, + { + type: "tool-input-delta", + toolCallId: "call_3", + inputTextDelta: '{"path":"src/big.ts","content":"chunk"}', + } as never, + { + type: "tool-input-end", + toolCallId: "call_3", + } as never, + { + type: "tool-call", + toolCallId: "call_3", + toolName: "write_file", + input: { + path: "src/big.ts", + content: "chunk", + }, + } as never, + ]); + + expect(output).toContain("call_3"); + expect(output).toContain("src/big.ts"); + }); + + it("keeps reasoning visible after tool blocks in stream order", async () => { + const { output } = await renderParts([ + { type: "reasoning-start", id: "reason_before" } as never, + { type: "reasoning-delta", id: "reason_before", text: "Before tool" }, + { type: "reasoning-end", id: "reason_before" } as never, + { + type: "tool-input-start", + id: "call_reason", + toolName: "bash", + }, + { + type: "tool-input-delta", + id: "call_reason", + delta: '{"command":"ls"}', + }, + { type: "tool-input-end", id: "call_reason" }, + { + type: "tool-call", + toolCallId: "call_reason", + toolName: "bash", + input: { + command: "ls", + }, + }, + { type: "reasoning-start", id: "reason_after" } as never, + { type: "reasoning-delta", id: "reason_after", text: "After tool" }, + { type: "reasoning-end", id: "reason_after" } as never, + ]); + + const beforeIndex = output.indexOf("Before tool"); + const toolIndex = output.indexOf("call_reason"); + const afterIndex = output.indexOf("After tool"); + + expect(beforeIndex).toBeGreaterThan(-1); + expect(toolIndex).toBeGreaterThan(-1); + expect(afterIndex).toBeGreaterThan(-1); + expect(beforeIndex).toBeLessThan(toolIndex); + expect(toolIndex).toBeLessThan(afterIndex); + }); + + it("keeps reasoning visible between two tool calls", async () => { + const { output } = await renderParts([ + { + type: "tool-input-start", + id: "call_a", + toolName: "bash", + }, + { + type: "tool-input-delta", + id: "call_a", + delta: '{"command":"pwd"}', + }, + { type: "tool-input-end", id: "call_a" }, + { + type: "tool-call", + toolCallId: "call_a", + toolName: "bash", + input: { + command: "pwd", + }, + }, + { type: "reasoning-start", id: "reason_mid" } as never, + { type: "reasoning-delta", id: "reason_mid", text: "Between tools" }, + { type: "reasoning-end", id: "reason_mid" } as never, + { + type: "tool-input-start", + id: "call_b", + toolName: "bash", + }, + { + type: "tool-input-delta", + id: "call_b", + delta: '{"command":"ls"}', + }, + { type: "tool-input-end", id: "call_b" }, + { + type: "tool-call", + toolCallId: "call_b", + toolName: "bash", + input: { + command: "ls", + }, + }, + ]); + + const firstToolIndex = output.indexOf("call_a"); + const reasoningIndex = output.indexOf("Between tools"); + const secondToolIndex = output.indexOf("call_b"); + + expect(firstToolIndex).toBeGreaterThan(-1); + expect(reasoningIndex).toBeGreaterThan(-1); + expect(secondToolIndex).toBeGreaterThan(-1); + expect(firstToolIndex).toBeLessThan(reasoningIndex); + expect(reasoningIndex).toBeLessThan(secondToolIndex); + }); + + it("keeps reasoning visible across unknown stream parts", async () => { + const { output } = await renderParts([ + { type: "reasoning-start", id: "reason_unknown_before" } as never, + { + type: "reasoning-delta", + id: "reason_unknown_before", + text: "Before unknown", + }, + { type: "reasoning-end", id: "reason_unknown_before" } as never, + { + type: "unknown-x", + payload: "mystery", + } as never, + { type: "reasoning-start", id: "reason_unknown_after" } as never, + { + type: "reasoning-delta", + id: "reason_unknown_after", + text: "After unknown", + }, + { type: "reasoning-end", id: "reason_unknown_after" } as never, + ]); + + const beforeIndex = output.indexOf("Before unknown"); + const unknownIndex = output.indexOf("[unknown part]"); + const afterIndex = output.indexOf("After unknown"); + + expect(beforeIndex).toBeGreaterThan(-1); + expect(unknownIndex).toBeGreaterThan(-1); + expect(afterIndex).toBeGreaterThan(-1); + expect(beforeIndex).toBeLessThan(unknownIndex); + expect(unknownIndex).toBeLessThan(afterIndex); + }); + + it("renders glob_files output as structured markdown", async () => { + const globOutput = [ + "OK - glob", + 'pattern: "src/**/*.ts"', + "path: /project", + "respect_git_ignore: true", + "file_count: 12", + "truncated: false", + "sorted_by: mtime desc", + "", + "======== glob results ========", + "/project/file1.ts", + "/project/file2.ts", + "/project/file3.ts", + "/project/file4.ts", + "/project/file5.ts", + "/project/file6.ts", + "/project/file7.ts", + "/project/file8.ts", + "/project/file9.ts", + "/project/file10.ts", + "/project/file11.ts", + "/project/file12.ts", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_glob", + toolName: "glob_files", + input: { + pattern: "src/**/*.ts", + }, + }, + { + type: "tool-result", + toolCallId: "call_glob", + toolName: "glob_files", + input: { + pattern: "src/**/*.ts", + }, + output: globOutput, + }, + ]); + + expect(output).toContain("Glob src/**/*.ts"); + expect(output).toContain("/project/file1.ts"); + expect(output).toContain("... (2 more lines)"); + expect(output).not.toContain("Tool glob_files"); + expect(output).not.toContain("Output"); + }); + + it("renders glob_files no-match output in glob mode", async () => { + const globOutput = [ + "OK - glob (no matches)", + 'pattern: "*.xyz"', + "path: /project", + "respect_git_ignore: true", + "file_count: 0", + "truncated: false", + "sorted_by: mtime desc", + "", + "======== glob results ========", + "(no matches)", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_glob_empty", + toolName: "glob_files", + input: { + pattern: "*.xyz", + }, + }, + { + type: "tool-result", + toolCallId: "call_glob_empty", + toolName: "glob_files", + input: { + pattern: "*.xyz", + }, + output: globOutput, + }, + ]); + + expect(output).toContain("Glob *.xyz"); + expect(output).toContain("(no matches)"); + expect(output).not.toContain("Tool glob_files"); + expect(output).not.toContain("Output"); + }); + + it("shows truncated marker for glob files when model truncates", async () => { + const globOutput = [ + "OK - glob", + 'pattern: "src/**/*.ts"', + "path: /project", + "respect_git_ignore: true", + "file_count: 12", + "truncated: true", + "sorted_by: mtime desc", + "", + "======== glob results ========", + "/project/file1.ts", + "/project/file2.ts", + "/project/file3.ts", + "/project/file4.ts", + "/project/file5.ts", + "/project/file6.ts", + "/project/file7.ts", + "/project/file8.ts", + "/project/file9.ts", + "/project/file10.ts", + "/project/file11.ts", + "/project/file12.ts", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_glob_truncated", + toolName: "glob_files", + input: { + pattern: "src/**/*.ts", + }, + }, + { + type: "tool-result", + toolCallId: "call_glob_truncated", + toolName: "glob_files", + input: { + pattern: "src/**/*.ts", + }, + output: globOutput, + }, + ]); + + expect(output).toContain("... (2 more lines, truncated)"); + expect(output).not.toContain("file_count ("); + expect(output).not.toContain("path: "); + }); + + it("renders grep_files output as structured markdown", async () => { + const grepOutput = [ + "OK - grep", + 'pattern: "foo"', + "path: /project", + "include: *.ts", + "case_sensitive: false", + "fixed_strings: false", + "match_count: 12", + "truncated: false", + "", + "======== grep results ========", + "/project/a.ts:1:const foo = 1;", + "/project/b.ts:2:const foo = 2;", + "/project/c.ts:3:const foo = 3;", + "/project/d.ts:4:const foo = 4;", + "/project/e.ts:5:const foo = 5;", + "/project/f.ts:6:const foo = 6;", + "/project/g.ts:7:const foo = 7;", + "/project/h.ts:8:const foo = 8;", + "/project/i.ts:9:const foo = 9;", + "/project/j.ts:10:const foo = 10;", + "/project/k.ts:11:const foo = 11;", + "/project/l.ts:12:const foo = 12;", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_grep", + toolName: "grep_files", + input: { + pattern: "foo", + }, + }, + { + type: "tool-result", + toolCallId: "call_grep", + toolName: "grep_files", + input: { + pattern: "foo", + }, + output: grepOutput, + }, + ]); + + expect(output).toContain("Grep foo"); + expect(output).toContain("/project/a.ts:1:const foo = 1;"); + expect(output).toContain("... (2 more lines)"); + expect(output).not.toContain("Tool grep_files"); + expect(output).not.toContain("Output"); + }); + + it("renders grep_files no-match output in grep mode", async () => { + const grepOutput = [ + "OK - grep (no matches)", + 'pattern: "foo"', + "path: /project", + "include: *.ts", + "case_sensitive: false", + "fixed_strings: false", + "match_count: 0", + "truncated: false", + "", + "======== grep results ========", + "(no matches)", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_grep_empty", + toolName: "grep_files", + input: { + pattern: "foo", + }, + }, + { + type: "tool-result", + toolCallId: "call_grep_empty", + toolName: "grep_files", + input: { + pattern: "foo", + }, + output: grepOutput, + }, + ]); + + expect(output).toContain("Grep foo"); + expect(output).toContain("(no matches)"); + expect(output).not.toContain("Tool grep_files"); + expect(output).not.toContain("Output"); + }); + + it("shows truncated marker for grep files when model truncates", async () => { + const grepOutput = [ + "OK - grep", + 'pattern: "foo"', + "path: /project", + "include: *.ts", + "case_sensitive: false", + "fixed_strings: false", + "match_count: 40", + "truncated: true", + "", + "======== grep results ========", + "/project/a.ts:1:const foo = 1;", + "/project/b.ts:2:const foo = 2;", + "/project/c.ts:3:const foo = 3;", + "/project/d.ts:4:const foo = 4;", + "/project/e.ts:5:const foo = 5;", + "/project/f.ts:6:const foo = 6;", + "/project/g.ts:7:const foo = 7;", + "/project/h.ts:8:const foo = 8;", + "/project/i.ts:9:const foo = 9;", + "/project/j.ts:10:const foo = 10;", + "/project/k.ts:11:const foo = 11;", + "/project/l.ts:12:const foo = 12;", + "======== end ========", + ].join("\n"); + + const { output } = await renderParts([ + { + type: "tool-call", + toolCallId: "call_grep_truncated", + toolName: "grep_files", + input: { + pattern: "foo", + }, + }, + { + type: "tool-result", + toolCallId: "call_grep_truncated", + toolName: "grep_files", + input: { + pattern: "foo", + }, + output: grepOutput, + }, + ]); + + expect(output).toContain("... (30 more lines, truncated)"); + expect(output).toContain("match_count (40)"); + expect(output).toContain("truncated: true"); + }); +}); diff --git a/src/interaction/pi-tui-stream-renderer.ts b/src/interaction/pi-tui-stream-renderer.ts new file mode 100644 index 0000000..ea868eb --- /dev/null +++ b/src/interaction/pi-tui-stream-renderer.ts @@ -0,0 +1,1460 @@ +import { + Container, + Markdown, + type MarkdownTheme, + Spacer, + Text, + truncateToWidth, + visibleWidth, +} from "@mariozechner/pi-tui"; +import type { TextStreamPart, ToolSet } from "ai"; + +type StreamPart = TextStreamPart; + +interface ToolInputRenderState { + hasContent: boolean; + toolName: string; +} + +export interface PiTuiStreamRenderOptions { + chatContainer: Container; + markdownTheme: MarkdownTheme; + showFiles?: boolean; + showFinishReason?: boolean; + showReasoning?: boolean; + showSources?: boolean; + showSteps?: boolean; + showToolResults?: boolean; + ui: { + requestRender: () => void; + }; +} + +interface ToolInputPart { + id?: string; + toolCallId?: string; +} + +interface ToolInputDeltaPart extends ToolInputPart { + delta?: unknown; + inputTextDelta?: unknown; +} + +const addChatComponent = ( + chatContainer: Container, + component: Container | Text | Markdown, + options: { addLeadingSpacer?: boolean } = {} +): void => { + if (options.addLeadingSpacer ?? true) { + chatContainer.addChild(new Spacer(1)); + } + chatContainer.addChild(component); +}; + +const safeStringify = (value: unknown): string => { + if (typeof value === "string") { + return value; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +}; + +const renderCodeBlock = (language: string, value: unknown): string => { + const text = safeStringify(value).replace(TRAILING_NEWLINES, ""); + const longestFenceRun = Array.from( + text.matchAll(BACKTICK_FENCE_PATTERN) + ).reduce((max, match) => Math.max(max, match[0].length), 2); + const fence = "`".repeat(longestFenceRun + 1); + return `${fence}${language}\n${text}\n${fence}`; +}; + +const READ_FILE_SUCCESS_PREFIX = "OK - read file"; +const GLOB_SUCCESS_PREFIX = "OK - glob"; +const GREP_SUCCESS_PREFIX = "OK - grep"; +const READ_FILE_BLOCK_PREFIX = "======== "; +const READ_FILE_BLOCK_SUFFIX = " ========"; +const READ_FILE_BLOCK_END = "======== end ========"; +const BACKTICK_FENCE_PATTERN = /`{3,}/g; +const READ_FILE_LINE_SPLIT_PATTERN = /^(\s*\d+\s\|\s)(.*)$/; +const READ_FILE_LINES_WITH_RETURNED_PATTERN = /^(\d+)\s+\(returned:\s*(\d+)\)$/; +const READ_FILE_MARKDOWN_FENCE_PATTERN = /^(?:`{3,}|~{3,}).*$/; +const SURROUNDED_BY_DOUBLE_QUOTES_PATTERN = /^"(.*)"$/; +const TAB_PATTERN = /\t/g; +const MAX_READ_PREVIEW_LINES = 10; + +interface ReadFileParsedOutput { + blockBody: string; + blockTitle: string; + metadata: Map; +} + +const extractStringField = (input: unknown, field: string): string | null => { + if (typeof input !== "object" || input === null) { + return null; + } + const record = input as Record; + return typeof record[field] === "string" ? (record[field] as string) : null; +}; + +const extractReadFilePath = (input: unknown): string | null => + extractStringField(input, "path"); + +const extractGlobPattern = (input: unknown): string | null => + extractStringField(input, "pattern"); + +const extractGrepPattern = (input: unknown): string | null => + extractStringField(input, "pattern"); + +const shouldIncludeReadFilePreviewLine = (line: string): boolean => { + const match = line.match(READ_FILE_LINE_SPLIT_PATTERN); + const content = (match?.[2] ?? line).trim(); + return !READ_FILE_MARKDOWN_FENCE_PATTERN.test(content); +}; + +interface ReadFileRenderPayload { + body: string; + path: string; + range: string | null; +} + +interface GlobRenderPayload { + body: string; + pattern: string; +} + +interface GrepRenderPayload { + body: string; + pattern: string; +} + +interface GlobPreviewMetadata { + truncated: boolean; +} + +interface GrepPreviewMetadata { + matchCount: string | null; + path: string | null; + truncated: boolean; +} + +const stripSurroundedDoubleQuotes = (value: string): string => { + const trimmed = value.trim(); + const matched = trimmed.match(SURROUNDED_BY_DOUBLE_QUOTES_PATTERN); + return matched?.[1] ?? trimmed; +}; + +const parseReadFileMetadataLine = ( + line: string +): { key: string; value: string } | null => { + const separatorIndex = line.indexOf(":"); + if (separatorIndex <= 0) { + return null; + } + + const key = line.slice(0, separatorIndex).trim(); + if (key.length === 0) { + return null; + } + + return { + key, + value: line.slice(separatorIndex + 1).trim(), + }; +}; + +const parseNumberedBlockToolOutput = ( + output: string, + successPrefix: string +): ReadFileParsedOutput | null => { + const normalized = output.replaceAll("\r\n", "\n"); + if (!normalized.startsWith(successPrefix)) { + return null; + } + + const lines = normalized.split("\n"); + const metadata = new Map(); + let blockStartIndex = -1; + + for (let index = 1; index < lines.length; index += 1) { + const line = lines[index]; + + if ( + line.startsWith(READ_FILE_BLOCK_PREFIX) && + line.endsWith(READ_FILE_BLOCK_SUFFIX) && + line !== READ_FILE_BLOCK_END + ) { + blockStartIndex = index; + break; + } + + if (line.trim().length === 0) { + continue; + } + + const parsed = parseReadFileMetadataLine(line); + if (!parsed) { + return null; + } + metadata.set(parsed.key, parsed.value); + } + + if (blockStartIndex < 0) { + return null; + } + + const blockEndIndex = lines.findIndex( + (line, index) => index > blockStartIndex && line === READ_FILE_BLOCK_END + ); + if (blockEndIndex < 0) { + return null; + } + + const blockTitle = lines[blockStartIndex] + .slice(READ_FILE_BLOCK_PREFIX.length, -READ_FILE_BLOCK_SUFFIX.length) + .trim(); + const blockBody = lines.slice(blockStartIndex + 1, blockEndIndex).join("\n"); + + return { + metadata, + blockTitle, + blockBody, + }; +}; + +const parseReadFileOutput = (output: string): ReadFileParsedOutput | null => { + return parseNumberedBlockToolOutput(output, READ_FILE_SUCCESS_PREFIX); +}; + +const parseGlobOutput = (output: string): ReadFileParsedOutput | null => { + return parseNumberedBlockToolOutput(output, GLOB_SUCCESS_PREFIX); +}; + +const parseGrepOutput = (output: string): ReadFileParsedOutput | null => { + return parseNumberedBlockToolOutput(output, GREP_SUCCESS_PREFIX); +}; + +const resolveReadPath = (parsed: ReadFileParsedOutput): string => { + const pathValue = parsed.metadata.get("path") ?? ""; + return pathValue.trim() || parsed.blockTitle || "(unknown)"; +}; + +const getToolOmittedLineCount = (metadata: Map): number => { + const linesMetadata = metadata.get("lines"); + if (!linesMetadata) { + return 0; + } + + const matchedCounts = linesMetadata.match( + READ_FILE_LINES_WITH_RETURNED_PATTERN + ); + if (!matchedCounts) { + return 0; + } + + const totalLines = Number.parseInt(matchedCounts[1], 10); + const returnedLines = Number.parseInt(matchedCounts[2], 10); + if (!(Number.isFinite(totalLines) && Number.isFinite(returnedLines))) { + return 0; + } + + const isTruncated = metadata.get("truncated")?.toLowerCase() === "true"; + if (!isTruncated) { + return 0; + } + + return Math.max(0, totalLines - returnedLines); +}; + +const buildReadPreviewLines = ( + visibleLines: string[], + totalOmitted: number, + isModelTruncated: boolean +): string[] => { + const previewLines = + visibleLines.length > 0 && visibleLines.some((line) => line.length > 0) + ? [...visibleLines] + : ["(empty)"]; + + if (totalOmitted <= 0) { + return previewLines; + } + + const lineLabel = `line${totalOmitted === 1 ? "" : "s"}`; + previewLines.push(""); + previewLines.push( + isModelTruncated + ? `... (${totalOmitted} more ${lineLabel}, truncated)` + : `... (${totalOmitted} more ${lineLabel})` + ); + + return previewLines; +}; + +const parseIntegerMetadataValue = ( + rawValue: string | undefined +): number | null => { + if (!rawValue) { + return null; + } + + const parsed = Number.parseInt(rawValue, 10); + return Number.isFinite(parsed) ? parsed : null; +}; + +const buildGlobPreviewLines = ( + visibleLines: string[], + totalOmitted: number, + metadata: GlobPreviewMetadata +): string[] => { + const previewLines = + visibleLines.length > 0 && visibleLines.some((line) => line.length > 0) + ? [...visibleLines] + : ["(no matches)"]; + + if (totalOmitted <= 0) { + return previewLines; + } + + const lineLabel = `line${totalOmitted === 1 ? "" : "s"}`; + + previewLines.push(""); + previewLines.push( + metadata.truncated + ? `... (${totalOmitted} more ${lineLabel}, truncated)` + : `... (${totalOmitted} more ${lineLabel})` + ); + + return previewLines; +}; + +const buildGrepPreviewLines = ( + visibleLines: string[], + totalOmitted: number, + metadata: GrepPreviewMetadata +): string[] => { + const previewLines = + visibleLines.length > 0 && visibleLines.some((line) => line.length > 0) + ? [...visibleLines] + : ["(no matches)"]; + + if (totalOmitted <= 0) { + return previewLines; + } + + const lineLabel = `line${totalOmitted === 1 ? "" : "s"}`; + const metadataParts = [`path: ${metadata.path ?? "."}`]; + if (metadata.matchCount) { + metadataParts.push(`match_count (${metadata.matchCount})`); + } + if (metadata.truncated) { + metadataParts.push("truncated: true"); + } + + previewLines.push(""); + previewLines.push( + metadata.truncated + ? `... (${totalOmitted} more ${lineLabel}, truncated)` + : `... (${totalOmitted} more ${lineLabel})` + ); + previewLines.push(metadataParts.join(", ")); + + return previewLines; +}; + +const renderReadFileOutput = (output: string): ReadFileRenderPayload | null => { + const parsed = parseReadFileOutput(output); + if (!parsed) { + return null; + } + + const readPath = resolveReadPath(parsed); + + const contentBody = + parsed.blockBody.trim().length > 0 ? parsed.blockBody : "(empty)"; + const allLines = contentBody + .split("\n") + .filter(shouldIncludeReadFilePreviewLine); + const omittedFromPreview = Math.max( + 0, + allLines.length - MAX_READ_PREVIEW_LINES + ); + const visibleLines = + omittedFromPreview > 0 + ? allLines.slice(0, MAX_READ_PREVIEW_LINES) + : allLines; + + const omittedFromTool = getToolOmittedLineCount(parsed.metadata); + const isModelTruncated = + parsed.metadata.get("truncated")?.toLowerCase() === "true"; + const totalOmitted = omittedFromPreview + omittedFromTool; + const previewLines = buildReadPreviewLines( + visibleLines, + totalOmitted, + isModelTruncated + ); + const rangeValue = parsed.metadata.get("range")?.trim() || null; + + return { + path: readPath, + range: rangeValue, + body: previewLines.join("\n"), + }; +}; + +const renderGlobOutput = (output: string): GlobRenderPayload | null => { + const parsed = parseGlobOutput(output); + if (!parsed) { + return null; + } + + const contentBody = parsed.blockBody.trim(); + const allLines = + contentBody.length > 0 + ? contentBody.split("\n").filter((line) => line.trim().length > 0) + : []; + + const omittedFromPreview = Math.max( + 0, + allLines.length - MAX_READ_PREVIEW_LINES + ); + const visibleLines = + omittedFromPreview > 0 + ? allLines.slice(0, MAX_READ_PREVIEW_LINES) + : allLines; + + const fileCountRaw = parsed.metadata.get("file_count"); + const fileCount = parseIntegerMetadataValue(fileCountRaw); + const isToolTruncated = + parsed.metadata.get("truncated")?.toLowerCase() === "true"; + const omittedFromTool = + isToolTruncated && fileCount !== null + ? Math.max(0, fileCount - allLines.length) + : 0; + const totalOmitted = omittedFromPreview + omittedFromTool; + + const metadata: GlobPreviewMetadata = { + truncated: isToolTruncated, + }; + + const previewLines = buildGlobPreviewLines( + visibleLines, + totalOmitted, + metadata + ); + const patternValue = + stripSurroundedDoubleQuotes(parsed.metadata.get("pattern") ?? "") || + parsed.blockTitle || + "(unknown)"; + + return { + pattern: patternValue, + body: previewLines.join("\n"), + }; +}; + +const renderGrepOutput = (output: string): GrepRenderPayload | null => { + const parsed = parseGrepOutput(output); + if (!parsed) { + return null; + } + + const contentBody = parsed.blockBody.trim(); + const allLines = contentBody.length > 0 ? contentBody.split("\n") : []; + + const omittedFromPreview = Math.max( + 0, + allLines.length - MAX_READ_PREVIEW_LINES + ); + const visibleLines = + omittedFromPreview > 0 + ? allLines.slice(0, MAX_READ_PREVIEW_LINES) + : allLines; + + const matchCountRaw = parsed.metadata.get("match_count"); + const matchCount = parseIntegerMetadataValue(matchCountRaw); + const isToolTruncated = + parsed.metadata.get("truncated")?.toLowerCase() === "true"; + const omittedFromTool = + isToolTruncated && matchCount !== null + ? Math.max(0, matchCount - allLines.length) + : 0; + const totalOmitted = omittedFromPreview + omittedFromTool; + + const metadata: GrepPreviewMetadata = { + path: parsed.metadata.get("path") ?? null, + matchCount: matchCountRaw ?? null, + truncated: isToolTruncated, + }; + + const previewLines = buildGrepPreviewLines( + visibleLines, + totalOmitted, + metadata + ); + + const patternValue = + stripSurroundedDoubleQuotes(parsed.metadata.get("pattern") ?? "") || + parsed.blockTitle || + "(unknown)"; + + return { + pattern: patternValue, + body: previewLines.join("\n"), + }; +}; + +const renderReadFilePendingOutput = (_path: string): string => { + return "Reading..."; +}; + +const renderGlobPendingOutput = (_pattern: string): string => { + return "Searching..."; +}; + +const renderGrepPendingOutput = (_pattern: string): string => { + return "Searching..."; +}; + +const renderToolOutput = (_toolName: string, output: unknown): string => { + return renderCodeBlock("text", output); +}; + +const ANSI_RESET = "\x1b[0m"; +const ANSI_DIM = "\x1b[2m"; +const ANSI_ITALIC = "\x1b[3m"; +const ANSI_GRAY = "\x1b[90m"; +const ANSI_BG_GRAY = "\x1b[100m"; +const LEADING_NEWLINES = /^\n+/; +const TRAILING_NEWLINES = /\n+$/; + +const applyReadPreviewBackground = (text: string): string => { + return `${ANSI_BG_GRAY}${text}${ANSI_RESET}`; +}; + +const styleThinkingText = (text: string): string => { + return `${ANSI_DIM}${ANSI_ITALIC}${ANSI_GRAY}${text}${ANSI_RESET}`; +}; + +class TrimmedMarkdown extends Markdown { + override render(width: number): string[] { + const lines = super.render(width); + let end = lines.length; + while (end > 0 && lines[end - 1].trim().length === 0) { + end -= 1; + } + return lines.slice(0, end); + } +} + +class TruncatedReadBody { + private cachedLines?: string[]; + private cachedText?: string; + private cachedWidth?: number; + private readonly background?: (text: string) => string; + private readonly paddingX: number; + private text: string; + + constructor( + text: string, + paddingX: number, + background?: (text: string) => string + ) { + this.text = text; + this.paddingX = paddingX; + this.background = background; + } + + setText(text: string): void { + this.text = text; + this.cachedText = undefined; + this.cachedWidth = undefined; + this.cachedLines = undefined; + } + + invalidate(): void { + this.cachedText = undefined; + this.cachedWidth = undefined; + this.cachedLines = undefined; + } + + render(width: number): string[] { + if ( + this.cachedLines && + this.cachedText === this.text && + this.cachedWidth === width + ) { + return this.cachedLines; + } + + if (!this.text || this.text.trim().length === 0) { + this.cachedText = this.text; + this.cachedWidth = width; + this.cachedLines = []; + return []; + } + + const normalizedText = this.text.replace(TAB_PATTERN, " "); + const contentWidth = Math.max(1, width - this.paddingX * 2); + const leftMargin = " ".repeat(this.paddingX); + const rightMargin = " ".repeat(this.paddingX); + + const renderedLines = normalizedText.split("\n").map((line) => { + const truncatedLine = truncateToWidth(line, contentWidth, ""); + const lineWithMargins = `${leftMargin}${truncatedLine}${rightMargin}`; + const visibleLength = visibleWidth(lineWithMargins); + const paddedLine = `${lineWithMargins}${" ".repeat(Math.max(0, width - visibleLength))}`; + + return this.background ? this.background(paddedLine) : paddedLine; + }); + + this.cachedText = this.text; + this.cachedWidth = width; + this.cachedLines = renderedLines; + return renderedLines; + } +} + +interface DiffLine { + text: string; + type: "add" | "context" | "delete"; +} + +const MAX_DIFF_MATRIX_CELLS = 60_000; +const MAX_DIFF_RENDER_LINES = 160; + +const buildSimpleDiff = (before: string, after: string): DiffLine[] => { + const beforeLines = before.split("\n"); + const afterLines = after.split("\n"); + const lines: DiffLine[] = []; + const maxLength = Math.max(beforeLines.length, afterLines.length); + + for (let index = 0; index < maxLength; index += 1) { + const oldLine = beforeLines[index]; + const newLine = afterLines[index]; + + if (oldLine === undefined && newLine !== undefined) { + lines.push({ type: "add", text: newLine }); + continue; + } + + if (newLine === undefined && oldLine !== undefined) { + lines.push({ type: "delete", text: oldLine }); + continue; + } + + if (oldLine === newLine && oldLine !== undefined) { + lines.push({ type: "context", text: oldLine }); + continue; + } + + if (oldLine !== undefined) { + lines.push({ type: "delete", text: oldLine }); + } + if (newLine !== undefined) { + lines.push({ type: "add", text: newLine }); + } + } + + return lines; +}; + +const buildLcsDiff = (before: string, after: string): DiffLine[] => { + const beforeLines = before.split("\n"); + const afterLines = after.split("\n"); + + if (beforeLines.length === 0 && afterLines.length === 0) { + return []; + } + + const matrixCells = (beforeLines.length + 1) * (afterLines.length + 1); + if (matrixCells > MAX_DIFF_MATRIX_CELLS) { + return buildSimpleDiff(before, after); + } + + const lcs: number[][] = new Array(beforeLines.length + 1) + .fill(undefined) + .map(() => new Array(afterLines.length + 1).fill(0)); + + for (let i = beforeLines.length - 1; i >= 0; i -= 1) { + for (let j = afterLines.length - 1; j >= 0; j -= 1) { + if (beforeLines[i] === afterLines[j]) { + lcs[i][j] = lcs[i + 1][j + 1] + 1; + } else { + lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]); + } + } + } + + const lines: DiffLine[] = []; + let i = 0; + let j = 0; + + while (i < beforeLines.length && j < afterLines.length) { + if (beforeLines[i] === afterLines[j]) { + lines.push({ type: "context", text: beforeLines[i] }); + i += 1; + j += 1; + continue; + } + + if (lcs[i + 1][j] >= lcs[i][j + 1]) { + lines.push({ type: "delete", text: beforeLines[i] }); + i += 1; + continue; + } + + lines.push({ type: "add", text: afterLines[j] }); + j += 1; + } + + while (i < beforeLines.length) { + lines.push({ type: "delete", text: beforeLines[i] }); + i += 1; + } + + while (j < afterLines.length) { + lines.push({ type: "add", text: afterLines[j] }); + j += 1; + } + + return lines; +}; + +const renderDiffBlock = (before: string, after: string): string => { + const diffLines = buildLcsDiff(before, after); + + const rendered: string[] = ["```diff"]; + for (let index = 0; index < diffLines.length; index += 1) { + if (index >= MAX_DIFF_RENDER_LINES) { + rendered.push("... diff truncated ..."); + break; + } + + const line = diffLines[index]; + if (line.type === "add") { + rendered.push(`+${line.text}`); + } else if (line.type === "delete") { + rendered.push(`-${line.text}`); + } else { + rendered.push(` ${line.text}`); + } + } + rendered.push("```"); + + return rendered.join("\n"); +}; + +const tryExtractEditPayload = ( + toolName: string, + input: unknown +): { newStr: string; oldStr: string; path?: string } | null => { + if (toolName !== "edit_file") { + return null; + } + + if (typeof input !== "object" || input === null) { + return null; + } + + const record = input as Record; + const oldStr = record.old_str; + const newStr = record.new_str; + const path = record.path; + + if (typeof oldStr !== "string" || typeof newStr !== "string") { + return null; + } + + return { + oldStr, + newStr, + path: typeof path === "string" ? path : undefined, + }; +}; + +class AssistantStreamView extends Container { + private readonly markdownTheme: MarkdownTheme; + private readonly segments: Array<{ + content: string; + type: "reasoning" | "text"; + }> = []; + + constructor(markdownTheme: MarkdownTheme) { + super(); + this.markdownTheme = markdownTheme; + this.refresh(); + } + + appendReasoning(delta: string): void { + this.appendSegment("reasoning", delta); + } + + appendText(delta: string): void { + this.appendSegment("text", delta); + } + + private appendSegment(type: "reasoning" | "text", delta: string): void { + if (delta.length === 0) { + return; + } + + const lastSegment = this.segments.at(-1); + if (lastSegment && lastSegment.type === type) { + lastSegment.content += delta; + } else { + this.segments.push({ + type, + content: delta, + }); + } + + this.refresh(); + } + + private refresh(): void { + this.clear(); + + const visibleSegments = this.segments + .map((segment) => { + const normalizedContent = + segment.type === "reasoning" + ? segment.content.replace(LEADING_NEWLINES, "").trimEnd() + : segment.content.trim(); + + return { + ...segment, + content: normalizedContent, + }; + }) + .filter((segment) => segment.content.trim().length > 0); + + if (visibleSegments.length === 0) { + return; + } + + for (let index = 0; index < visibleSegments.length; index += 1) { + const segment = visibleSegments[index]; + const text = segment.content; + + if (segment.type === "text") { + this.addChild(new Markdown(text, 1, 0, this.markdownTheme)); + } else { + this.addChild( + new Markdown(text, 1, 0, this.markdownTheme, { + color: styleThinkingText, + italic: true, + }) + ); + } + + if (index < visibleSegments.length - 1) { + this.addChild(new Spacer(1)); + } + } + } +} + +class ToolCallView extends Container { + private readonly callId: string; + private readonly content: TrimmedMarkdown; + private readonly readBlock: Container; + private readonly readBody: TruncatedReadBody; + private readonly readHeader: TrimmedMarkdown; + private error: unknown; + private finalInput: unknown; + private inputBuffer = ""; + private output: unknown; + private outputDenied = false; + private parsedInput: unknown; + private readMode = false; + private toolName: string; + + constructor(callId: string, toolName: string, markdownTheme: MarkdownTheme) { + super(); + this.callId = callId; + this.toolName = toolName; + this.content = new TrimmedMarkdown("", 1, 0, markdownTheme); + this.readHeader = new TrimmedMarkdown("", 1, 0, markdownTheme); + this.readBody = new TruncatedReadBody("", 1, applyReadPreviewBackground); + this.readBlock = new Container(); + this.readBlock.addChild(this.readHeader); + this.readBlock.addChild(new Spacer(1)); + this.readBlock.addChild(this.readBody); + this.addChild(this.content); + this.refresh(); + } + + private setReadMode(enabled: boolean): void { + if (this.readMode === enabled) { + return; + } + + this.readMode = enabled; + this.clear(); + this.addChild(enabled ? this.readBlock : this.content); + } + + appendInputChunk(chunk: string): void { + this.inputBuffer += chunk; + try { + this.parsedInput = JSON.parse(this.inputBuffer) as unknown; + } catch { + this.parsedInput = undefined; + } + this.refresh(); + } + + setError(error: unknown): void { + this.error = error; + this.refresh(); + } + + setFinalInput(input: unknown): void { + this.finalInput = input; + this.refresh(); + } + + setOutput(output: unknown): void { + this.output = output; + this.refresh(); + } + + setOutputDenied(): void { + this.outputDenied = true; + this.refresh(); + } + + setToolName(toolName: string): void { + this.toolName = toolName; + this.refresh(); + } + + private resolveBestInput(): unknown { + if (this.finalInput !== undefined) { + return this.finalInput; + } + if (this.parsedInput !== undefined) { + return this.parsedInput; + } + if (this.inputBuffer.length > 0) { + return this.inputBuffer; + } + return undefined; + } + + private tryRenderReadFileMode(): boolean { + if ( + this.toolName !== "read_file" || + this.error !== undefined || + this.outputDenied + ) { + return false; + } + + const bestInput = this.resolveBestInput(); + const readPath = extractReadFilePath(bestInput); + + if (typeof this.output === "string") { + const renderedReadFile = renderReadFileOutput(this.output); + this.setReadMode(true); + if (renderedReadFile) { + const pathWithRange = renderedReadFile.range + ? `${renderedReadFile.path} ${renderedReadFile.range}` + : renderedReadFile.path; + this.readHeader.setText(`**Read** \`${pathWithRange}\``); + this.readBody.setText(renderedReadFile.body); + } else { + const fallbackPath = readPath ?? "(unknown)"; + this.readHeader.setText(`**Read** \`${fallbackPath}\``); + this.readBody.setText(safeStringify(this.output)); + } + return true; + } + + if (!readPath) { + return false; + } + + this.setReadMode(true); + this.readHeader.setText(`**Read** \`${readPath}\``); + this.readBody.setText(renderReadFilePendingOutput(readPath)); + return true; + } + + private tryRenderGlobMode(): boolean { + if ( + this.toolName !== "glob_files" || + this.error !== undefined || + this.outputDenied + ) { + return false; + } + + const bestInput = this.resolveBestInput(); + const globPattern = extractGlobPattern(bestInput); + + if (typeof this.output === "string") { + const renderedGlob = renderGlobOutput(this.output); + this.setReadMode(true); + if (renderedGlob) { + this.readHeader.setText(`**Glob** \`${renderedGlob.pattern}\``); + this.readBody.setText(renderedGlob.body); + } else { + const fallbackPattern = globPattern ?? "(unknown)"; + this.readHeader.setText(`**Glob** \`${fallbackPattern}\``); + this.readBody.setText(safeStringify(this.output)); + } + return true; + } + + if (!globPattern) { + return false; + } + + this.setReadMode(true); + this.readHeader.setText(`**Glob** \`${globPattern}\``); + this.readBody.setText(renderGlobPendingOutput(globPattern)); + return true; + } + + private tryRenderGrepMode(): boolean { + if ( + this.toolName !== "grep_files" || + this.error !== undefined || + this.outputDenied + ) { + return false; + } + + const bestInput = this.resolveBestInput(); + const grepPattern = extractGrepPattern(bestInput); + + if (typeof this.output === "string") { + const renderedGrep = renderGrepOutput(this.output); + this.setReadMode(true); + if (renderedGrep) { + this.readHeader.setText(`**Grep** \`${renderedGrep.pattern}\``); + this.readBody.setText(renderedGrep.body); + } else { + const fallbackPattern = grepPattern ?? "(unknown)"; + this.readHeader.setText(`**Grep** \`${fallbackPattern}\``); + this.readBody.setText(safeStringify(this.output)); + } + return true; + } + + if (!grepPattern) { + return false; + } + + this.setReadMode(true); + this.readHeader.setText(`**Grep** \`${grepPattern}\``); + this.readBody.setText(renderGrepPendingOutput(grepPattern)); + return true; + } + + private refresh(): void { + if ( + this.tryRenderReadFileMode() || + this.tryRenderGlobMode() || + this.tryRenderGrepMode() + ) { + return; + } + + this.setReadMode(false); + + const blocks: string[] = [ + `**Tool** \`${this.toolName}\` (\`${this.callId}\`)`, + ]; + + const bestInput = this.resolveBestInput(); + if (bestInput !== undefined) { + blocks.push(`**Input**\n\n${renderCodeBlock("json", bestInput)}`); + + const editPayload = tryExtractEditPayload(this.toolName, bestInput); + if (editPayload) { + const heading = editPayload.path + ? `**Live diff preview** (\`${editPayload.path}\`)` + : "**Live diff preview**"; + blocks.push( + `${heading}\n\n${renderDiffBlock(editPayload.oldStr, editPayload.newStr)}` + ); + } + } + + if (this.output !== undefined) { + blocks.push( + `**Output**\n\n${renderToolOutput(this.toolName, this.output)}` + ); + } + + if (this.error !== undefined) { + blocks.push(`**Error**\n\n${renderCodeBlock("text", this.error)}`); + } + + if (this.outputDenied) { + blocks.push("**Output** denied by model/policy"); + } + + this.content.setText(blocks.join("\n\n")); + } +} + +const getToolInputId = (part: ToolInputPart): string | undefined => { + return part.id ?? part.toolCallId; +}; + +const getToolInputChunk = (part: ToolInputDeltaPart): string | null => { + if (typeof part.delta === "string") { + return part.delta; + } + + if (typeof part.inputTextDelta === "string") { + return part.inputTextDelta; + } + + return null; +}; + +const createInfoMessage = (title: string, value: unknown): Text => { + return new Text(`${title}\n${safeStringify(value)}`, 1, 0); +}; + +interface PiTuiRenderFlags { + showFiles: boolean; + showFinishReason: boolean; + showReasoning: boolean; + showSources: boolean; + showSteps: boolean; + showToolResults: boolean; +} + +interface PiTuiStreamState { + activeToolInputs: Map; + chatContainer: Container; + ensureAssistantView: () => AssistantStreamView; + ensureToolView: (toolCallId: string, toolName: string) => ToolCallView; + flags: PiTuiRenderFlags; + resetAssistantView: (suppressLeadingSpacer?: boolean) => void; + streamedToolCallIds: Set; +} + +type StreamPartHandler = (part: StreamPart, state: PiTuiStreamState) => void; + +const handleTextStart: StreamPartHandler = (_part, state) => { + state.ensureAssistantView(); +}; + +const handleTextDelta: StreamPartHandler = (part, state) => { + const textPart = part as Extract; + state.ensureAssistantView().appendText(textPart.text); +}; + +const handleReasoningStart: StreamPartHandler = (_part, state) => { + if (state.flags.showReasoning) { + state.ensureAssistantView(); + } +}; + +const handleReasoningDelta: StreamPartHandler = (part, state) => { + if (!state.flags.showReasoning) { + return; + } + const reasoningPart = part as Extract< + StreamPart, + { type: "reasoning-delta" } + >; + state.ensureAssistantView().appendReasoning(reasoningPart.text); +}; + +const handleToolInputStart: StreamPartHandler = (part, state) => { + const toolInputStartPart = part as Extract< + StreamPart, + { type: "tool-input-start" } + >; + const toolCallId = getToolInputId(toolInputStartPart); + if (!toolCallId) { + return; + } + + state.activeToolInputs.set(toolCallId, { + toolName: toolInputStartPart.toolName, + hasContent: false, + }); + state.streamedToolCallIds.add(toolCallId); + state.resetAssistantView(true); + state.ensureToolView(toolCallId, toolInputStartPart.toolName); +}; + +const handleToolInputDelta: StreamPartHandler = (part, state) => { + const toolInputDeltaPart = part as Extract< + StreamPart, + { type: "tool-input-delta" } + >; + const toolCallId = getToolInputId(toolInputDeltaPart); + if (!toolCallId) { + return; + } + + if (!state.activeToolInputs.has(toolCallId)) { + state.activeToolInputs.set(toolCallId, { + toolName: "tool", + hasContent: false, + }); + } + + const toolState = state.activeToolInputs.get(toolCallId); + const toolName = toolState?.toolName ?? "tool"; + state.resetAssistantView(true); + const toolView = state.ensureToolView(toolCallId, toolName); + const chunk = getToolInputChunk(toolInputDeltaPart); + + if (chunk) { + toolView.appendInputChunk(chunk); + if (toolState) { + toolState.hasContent = true; + } + } + + state.streamedToolCallIds.add(toolCallId); +}; + +const handleToolInputEnd: StreamPartHandler = (part, state) => { + const toolInputEndPart = part as Extract< + StreamPart, + { type: "tool-input-end" } + >; + const toolCallId = getToolInputId(toolInputEndPart); + if (toolCallId) { + state.streamedToolCallIds.add(toolCallId); + } +}; + +const handleToolCall: StreamPartHandler = (part, state) => { + const toolCallPart = part as Extract; + const inputState = state.activeToolInputs.get(toolCallPart.toolCallId); + const shouldSkipToolCallRender = + state.streamedToolCallIds.has(toolCallPart.toolCallId) && + inputState?.hasContent === true; + + state.activeToolInputs.delete(toolCallPart.toolCallId); + state.streamedToolCallIds.delete(toolCallPart.toolCallId); + + state.resetAssistantView(true); + const view = state.ensureToolView( + toolCallPart.toolCallId, + toolCallPart.toolName + ); + view.setFinalInput(toolCallPart.input); + + if (!shouldSkipToolCallRender) { + view.setToolName(toolCallPart.toolName); + } +}; + +const handleToolResult: StreamPartHandler = (part, state) => { + if (!state.flags.showToolResults) { + return; + } + + const toolResultPart = part as Extract; + state.resetAssistantView(true); + const view = state.ensureToolView( + toolResultPart.toolCallId, + toolResultPart.toolName + ); + view.setOutput(toolResultPart.output); +}; + +const handleToolError: StreamPartHandler = (part, state) => { + const toolErrorPart = part as Extract; + state.resetAssistantView(true); + const view = state.ensureToolView( + toolErrorPart.toolCallId, + toolErrorPart.toolName + ); + view.setError(toolErrorPart.error); +}; + +const handleToolOutputDenied: StreamPartHandler = (part, state) => { + const deniedPart = part as Extract< + StreamPart, + { type: "tool-output-denied" } + >; + state.resetAssistantView(true); + const view = state.ensureToolView(deniedPart.toolCallId, deniedPart.toolName); + view.setOutputDenied(); +}; + +const handleStartStep: StreamPartHandler = (_part, state) => { + if (!state.flags.showSteps) { + return; + } + state.resetAssistantView(); + addChatComponent(state.chatContainer, createInfoMessage("[step start]", "")); +}; + +const handleFinishStep: StreamPartHandler = (part, state) => { + if (!state.flags.showSteps) { + return; + } + const finishStepPart = part as Extract; + state.resetAssistantView(); + addChatComponent( + state.chatContainer, + createInfoMessage("[step finish]", finishStepPart.finishReason) + ); +}; + +const handleSource: StreamPartHandler = (part, state) => { + if (!state.flags.showSources) { + return; + } + state.resetAssistantView(); + addChatComponent(state.chatContainer, createInfoMessage("[source]", part)); +}; + +const handleFile: StreamPartHandler = (part, state) => { + if (!state.flags.showFiles) { + return; + } + const filePart = part as Extract; + state.resetAssistantView(); + addChatComponent( + state.chatContainer, + createInfoMessage("[file]", filePart.file) + ); +}; + +const handleFinish: StreamPartHandler = (part, state) => { + if (!state.flags.showFinishReason) { + return; + } + + const finishPart = part as Extract; + state.resetAssistantView(); + addChatComponent( + state.chatContainer, + createInfoMessage("[finish]", finishPart.finishReason ?? "unknown") + ); +}; + +const STREAM_HANDLERS: Record = { + "text-start": handleTextStart, + "text-delta": handleTextDelta, + "reasoning-start": handleReasoningStart, + "reasoning-delta": handleReasoningDelta, + "tool-input-start": handleToolInputStart, + "tool-input-delta": handleToolInputDelta, + "tool-input-end": handleToolInputEnd, + "tool-call": handleToolCall, + "tool-result": handleToolResult, + "tool-error": handleToolError, + "tool-output-denied": handleToolOutputDenied, + "start-step": handleStartStep, + "finish-step": handleFinishStep, + source: handleSource, + file: handleFile, + finish: handleFinish, +}; + +const IGNORE_PART_TYPES = new Set([ + "abort", + "text-end", + "reasoning-end", + "start", + "tool-approval-request", +]); + +const handleStreamPart = (part: StreamPart, state: PiTuiStreamState): void => { + const handler = STREAM_HANDLERS[part.type]; + if (handler) { + handler(part, state); + return; + } + + if (IGNORE_PART_TYPES.has(part.type)) { + return; + } + + state.resetAssistantView(); + addChatComponent( + state.chatContainer, + createInfoMessage("[unknown part]", part) + ); +}; + +export const renderFullStreamWithPiTui = async ( + stream: AsyncIterable>, + options: PiTuiStreamRenderOptions +): Promise => { + const flags: PiTuiRenderFlags = { + showReasoning: options.showReasoning ?? true, + showSteps: options.showSteps ?? false, + showFinishReason: options.showFinishReason ?? false, + showToolResults: options.showToolResults ?? true, + showSources: options.showSources ?? false, + showFiles: options.showFiles ?? false, + }; + + const activeToolInputs = new Map(); + const streamedToolCallIds = new Set(); + const toolViews = new Map(); + let assistantView: AssistantStreamView | null = null; + let suppressAssistantLeadingSpacer = false; + + const resetAssistantView = (suppressLeadingSpacer = false): void => { + if (suppressLeadingSpacer) { + suppressAssistantLeadingSpacer = true; + } + assistantView = null; + }; + + const ensureAssistantView = (): AssistantStreamView => { + if (!assistantView) { + assistantView = new AssistantStreamView(options.markdownTheme); + addChatComponent(options.chatContainer, assistantView, { + addLeadingSpacer: !suppressAssistantLeadingSpacer, + }); + suppressAssistantLeadingSpacer = false; + } + return assistantView; + }; + + const ensureToolView = ( + toolCallId: string, + toolName: string + ): ToolCallView => { + const existing = toolViews.get(toolCallId); + if (existing) { + existing.setToolName(toolName); + return existing; + } + + const view = new ToolCallView(toolCallId, toolName, options.markdownTheme); + toolViews.set(toolCallId, view); + addChatComponent(options.chatContainer, view); + return view; + }; + + const state: PiTuiStreamState = { + flags, + activeToolInputs, + streamedToolCallIds, + resetAssistantView, + ensureAssistantView, + ensureToolView, + chatContainer: options.chatContainer, + }; + + for await (const rawPart of stream) { + const part = rawPart as StreamPart; + + handleStreamPart(part, state); + options.ui.requestRender(); + } +}; diff --git a/src/interaction/spinner.ts b/src/interaction/spinner.ts index 6939236..9d89072 100644 --- a/src/interaction/spinner.ts +++ b/src/interaction/spinner.ts @@ -1,3 +1,9 @@ +let spinnerOutputEnabled = true; + +export const setSpinnerOutputEnabled = (enabled: boolean): void => { + spinnerOutputEnabled = enabled; +}; + export class Spinner { private interval: Timer | null = null; private readonly frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; @@ -13,6 +19,10 @@ export class Spinner { return; } + if (!spinnerOutputEnabled) { + return; + } + process.stdout.write("\x1B[?25l"); this.interval = setInterval(() => { const frame = this.frames[this.currentFrame]; @@ -26,6 +36,11 @@ export class Spinner { clearInterval(this.interval); this.interval = null; } + + if (!spinnerOutputEnabled) { + return; + } + process.stdout.write("\r\x1B[K"); process.stdout.write("\x1B[?25h"); } diff --git a/src/interaction/stdin-buffer.ts b/src/interaction/stdin-buffer.ts deleted file mode 100644 index 1cc6597..0000000 --- a/src/interaction/stdin-buffer.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { EventEmitter } from "node:events"; - -const ESC = "\x1b"; -const BRACKETED_PASTE_START = "\x1b[200~"; -const BRACKETED_PASTE_END = "\x1b[201~"; -const NUMERIC_REGEX = /^\d+$/; - -type SequenceStatus = "complete" | "incomplete" | "not-escape"; - -const isCompleteSequence = (data: string): SequenceStatus => { - if (!data.startsWith(ESC)) { - return "not-escape"; - } - if (data.length === 1) { - return "incomplete"; - } - - const afterEsc = data.slice(1); - if (afterEsc.startsWith("[")) { - if (afterEsc.startsWith("[M")) { - return data.length >= 6 ? "complete" : "incomplete"; - } - return isCompleteCsiSequence(data); - } - if (afterEsc.startsWith("]")) { - return isCompleteOscSequence(data); - } - if (afterEsc.startsWith("P")) { - return isCompleteDcsSequence(data); - } - if (afterEsc.startsWith("_")) { - return isCompleteApcSequence(data); - } - if (afterEsc.startsWith("O")) { - return afterEsc.length >= 2 ? "complete" : "incomplete"; - } - if (afterEsc.length === 1) { - return "complete"; - } - return "complete"; -}; - -const isCompleteCsiSequence = (data: string): "complete" | "incomplete" => { - if (!data.startsWith(`${ESC}[`)) { - return "complete"; - } - if (data.length < 3) { - return "incomplete"; - } - const payload = data.slice(2); - const lastChar = payload.at(-1); - if (!lastChar) { - return "incomplete"; - } - const lastCode = lastChar.charCodeAt(0); - if (lastCode >= 0x40 && lastCode <= 0x7e) { - if (payload.startsWith("<") && (lastChar === "m" || lastChar === "M")) { - const params = payload.slice(1, -1).split(";"); - const allNumeric = params.every((param) => NUMERIC_REGEX.test(param)); - return allNumeric ? "complete" : "incomplete"; - } - return "complete"; - } - return "incomplete"; -}; - -const isCompleteOscSequence = (data: string): "complete" | "incomplete" => { - if (!data.startsWith(`${ESC}]`)) { - return "complete"; - } - return data.endsWith(`${ESC}\\`) || data.endsWith("\x07") - ? "complete" - : "incomplete"; -}; - -const isCompleteDcsSequence = (data: string): "complete" | "incomplete" => { - if (!data.startsWith(`${ESC}P`)) { - return "complete"; - } - return data.endsWith(`${ESC}\\`) ? "complete" : "incomplete"; -}; - -const isCompleteApcSequence = (data: string): "complete" | "incomplete" => { - if (!data.startsWith(`${ESC}_`)) { - return "complete"; - } - return data.endsWith(`${ESC}\\`) ? "complete" : "incomplete"; -}; - -const extractCompleteSequences = ( - buffer: string -): { sequences: string[]; remainder: string } => { - const sequences: string[] = []; - let pos = 0; - - while (pos < buffer.length) { - const remaining = buffer.slice(pos); - if (remaining.startsWith(ESC)) { - 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; - } - if (status === "incomplete") { - seqEnd += 1; - continue; - } - sequences.push(candidate); - pos += seqEnd; - break; - } - if (seqEnd > remaining.length) { - return { sequences, remainder: remaining }; - } - } else { - sequences.push(remaining[0] ?? ""); - pos += 1; - } - } - - return { sequences, remainder: "" }; -}; - -export interface StdinBufferOptions { - timeout?: number; -} - -export class StdinBuffer extends EventEmitter { - private buffer = ""; - private timeout: ReturnType | null = null; - private readonly timeoutMs: number; - private pasteMode = false; - private pasteBuffer = ""; - - constructor(options: StdinBufferOptions = {}) { - super(); - this.timeoutMs = options.timeout ?? 10; - } - - private convertBufferToString(data: string | Buffer): string { - if (!Buffer.isBuffer(data)) { - return data; - } - - if (data.length === 1 && data[0] !== undefined && data[0] > 127) { - const byte = data[0] - 128; - return `\x1b${String.fromCharCode(byte)}`; - } - - return data.toString(); - } - - private processPasteModeBuffer(): void { - this.pasteBuffer += this.buffer; - this.buffer = ""; - const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END); - - if (endIndex === -1) { - return; - } - - 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); - } - } - - private handleBracketedPasteStart(startIndex: number): void { - 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); - } - } - } - - private processNormalInput(): void { - 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); - } - } - - process(data: string | Buffer): void { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - - const str = this.convertBufferToString(data); - - if (str.length === 0 && this.buffer.length === 0) { - this.emit("data", ""); - return; - } - - this.buffer += str; - - if (this.pasteMode) { - this.processPasteModeBuffer(); - return; - } - - const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START); - if (startIndex !== -1) { - this.handleBracketedPasteStart(startIndex); - return; - } - - this.processNormalInput(); - } - - flush(): string[] { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - if (this.buffer.length === 0) { - 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/src/interaction/stream-renderer.test.ts b/src/interaction/stream-renderer.test.ts deleted file mode 100644 index 7a4c59f..0000000 --- a/src/interaction/stream-renderer.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { Writable } from "node:stream"; -import type { TextStreamPart, ToolSet } from "ai"; -import { renderFullStream } from "./stream-renderer"; - -type TestStreamPart = TextStreamPart; - -const renderParts = async (parts: TestStreamPart[]): Promise => { - let output = ""; - - const writable = new Writable({ - write(chunk, _encoding, callback) { - output += String(chunk); - callback(); - }, - }); - - async function* stream(): AsyncIterable { - for (const part of parts) { - await Promise.resolve(); - yield part; - } - } - - await renderFullStream(stream(), { - output: writable, - showReasoning: false, - showSteps: false, - showFinishReason: false, - showToolResults: true, - showSources: false, - showFiles: false, - useColor: false, - smoothStream: false, - }); - - return output; -}; - -describe("renderFullStream tool input streaming", () => { - it("renders tool-input-delta in real time and avoids duplicate tool-call input", async () => { - const output = await renderParts([ - { - type: "tool-input-start", - id: "call_1", - toolName: "write_file", - }, - { - type: "tool-input-delta", - id: "call_1", - delta: '{"path":"src/big.ts",', - }, - { - type: "tool-input-delta", - id: "call_1", - delta: '"content":"chunk"}', - }, - { - type: "tool-input-end", - id: "call_1", - }, - { - type: "tool-call", - toolCallId: "call_1", - toolName: "write_file", - input: { - path: "src/big.ts", - content: "chunk", - }, - }, - ]); - - expect(output).toContain( - 'tool write_file (call_1)\ninput: {"path":"src/big.ts","content":"chunk"}\n' - ); - expect(output).not.toContain(' "path": "src/big.ts"'); - expect((output.match(/tool write_file \(call_1\)/g) ?? []).length).toBe(1); - }); - - it("supports toolCallId and inputTextDelta from AI SDK tool-input deltas", async () => { - const output = await renderParts([ - { - type: "tool-input-start", - toolCallId: "call_3", - toolName: "write_file", - } as never, - { - type: "tool-input-delta", - toolCallId: "call_3", - inputTextDelta: '{"path":"src/big.ts","content":"chunk"}', - } as never, - { - type: "tool-input-end", - toolCallId: "call_3", - } as never, - { - type: "tool-call", - toolCallId: "call_3", - toolName: "write_file", - input: { - path: "src/big.ts", - content: "chunk", - }, - } as never, - ]); - - expect(output).toContain( - 'tool write_file (call_3)\ninput: {"path":"src/big.ts","content":"chunk"}\n' - ); - expect((output.match(/tool write_file \(call_3\)/g) ?? []).length).toBe(1); - }); - - it("keeps existing tool-call rendering when no tool-input-delta exists", async () => { - const output = await renderParts([ - { - type: "tool-call", - toolCallId: "call_2", - toolName: "bash", - input: { - command: "ls -la", - }, - }, - ]); - - expect(output).toContain("tool bash (call_2)"); - expect(output).toContain('"command": "ls -la"'); - }); - - it("renders tool-call input when tool-input stream has no deltas", async () => { - const output = await renderParts([ - { - type: "tool-input-start", - id: "call_4", - toolName: "bash", - }, - { - type: "tool-input-end", - id: "call_4", - }, - { - type: "tool-call", - toolCallId: "call_4", - toolName: "bash", - input: { - command: "ls -la", - }, - }, - ]); - - expect((output.match(/tool bash \(call_4\)/g) ?? []).length).toBe(1); - expect(output).toContain("tool bash (call_4)"); - expect(output).toContain('"command": "ls -la"'); - }); -}); diff --git a/src/interaction/stream-renderer.ts b/src/interaction/stream-renderer.ts deleted file mode 100644 index 0ef29fb..0000000 --- a/src/interaction/stream-renderer.ts +++ /dev/null @@ -1,609 +0,0 @@ -import type { Writable } from "node:stream"; -import type { TextStreamPart, ToolSet } from "ai"; -import { env } from "../env"; -import { colorize, colors } from "./colors"; - -export interface StreamRenderOptions { - output?: Writable; - showFiles?: boolean; - showFinishReason?: boolean; - showReasoning?: boolean; - showSources?: boolean; - showSteps?: boolean; - showToolResults?: boolean; - smoothDelayMs?: number; - smoothStream?: boolean; - useColor?: boolean; -} - -type StreamMode = "text" | "reasoning" | "tool-input" | "none"; - -interface RenderContext { - activeToolInputs: Map; - output: Writable; - reasoningLineLength: number; - segmenter: Intl.Segmenter; - showFiles: boolean; - showFinishReason: boolean; - showReasoning: boolean; - showSources: boolean; - showSteps: boolean; - showToolResults: boolean; - smoothDelayMs: number; - smoothStream: boolean; - streamedToolCallIds: Set; - terminalWidth: number; - textBuffer: string; - useColor: boolean; -} - -interface ToolInputRenderState { - hasContent: boolean; - toolName: string; -} - -const getToolInputId = ( - part: - | Extract - | Extract - | Extract -): string | undefined => { - const anyPart = part as { - id?: string; - toolCallId?: string; - }; - - return anyPart.id ?? anyPart.toolCallId; -}; - -const getToolInputChunk = ( - part: Extract -): string | null => { - const anyPart = part as { - delta?: unknown; - inputTextDelta?: unknown; - }; - - if (typeof anyPart.delta === "string") { - return anyPart.delta; - } - - if (typeof anyPart.inputTextDelta === "string") { - return anyPart.inputTextDelta; - } - - return null; -}; - -type StreamPart = TextStreamPart; - -const formatBlock = (value: unknown): string => { - if (typeof value === "string") { - return value; - } - - try { - return JSON.stringify(value, null, 2); - } catch { - return String(value); - } -}; - -const write = (ctx: RenderContext, text: string): void => { - ctx.output.write(text); -}; - -const writeLine = (ctx: RenderContext, text = ""): void => { - ctx.output.write(`${text}\n`); -}; - -const applyColor = ( - ctx: RenderContext, - color: keyof typeof colors, - text: string -): string => { - if (!ctx.useColor) { - return text; - } - - return colorize(color, text); -}; - -const renderLabel = (ctx: RenderContext, label: string): string => { - return applyColor(ctx, "magenta", label); -}; - -const renderToolLabel = (ctx: RenderContext): string => { - if (!ctx.useColor) { - return "tool"; - } - return `${colors.bold}${colors.brightGreen}tool${colors.reset}`; -}; - -const renderErrorLabel = (ctx: RenderContext): string => { - if (!ctx.useColor) { - return "error"; - } - return `${colors.bold}${colors.red}error${colors.reset}`; -}; - -const renderReasoningPrefix = (ctx: RenderContext): string => { - if (!ctx.useColor) { - return "│ "; - } - - return `${colors.dim}${colors.italic}${colors.gray}│ `; -}; - -const renderReasoningEnd = (ctx: RenderContext): string => { - return ctx.useColor ? colors.reset : ""; -}; - -const handleTextStart = (ctx: RenderContext, mode: StreamMode): StreamMode => { - if (mode !== "text") { - writeLine(ctx); - const aiLabel = ctx.useColor - ? `${colors.bold}${colors.brightCyan}AI${colors.reset}` - : "AI"; - write(ctx, `${aiLabel}: `); - } - return "text"; -}; - -const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -const flushTextBuffer = async (ctx: RenderContext): Promise => { - if (!ctx.smoothStream || ctx.textBuffer.length === 0) { - if (ctx.textBuffer.length > 0) { - write(ctx, ctx.textBuffer); - ctx.textBuffer = ""; - } - return; - } - - const segments = ctx.segmenter.segment(ctx.textBuffer); - let flushed = ""; - - for (const { segment, isWordLike } of segments) { - flushed += segment; - if (isWordLike || segment.includes("\n")) { - write(ctx, flushed); - flushed = ""; - if (ctx.smoothDelayMs > 0) { - await sleep(ctx.smoothDelayMs); - } - } - } - - ctx.textBuffer = flushed; -}; - -const handleTextDelta = async ( - ctx: RenderContext, - part: Extract, - mode: StreamMode -): Promise => { - if (mode !== "text") { - writeLine(ctx); - const aiLabel = ctx.useColor - ? `${colors.bold}${colors.brightCyan}AI${colors.reset}` - : "AI"; - write(ctx, `${aiLabel}: `); - } - - if (ctx.smoothStream) { - ctx.textBuffer += part.text; - await flushTextBuffer(ctx); - } else { - write(ctx, part.text); - } - - return "text"; -}; - -const handleTextEnd = (ctx: RenderContext, mode: StreamMode): StreamMode => { - if (ctx.textBuffer.length > 0) { - write(ctx, ctx.textBuffer); - ctx.textBuffer = ""; - } - if (mode === "text") { - writeLine(ctx); - } - return "none"; -}; - -const REASONING_PREFIX_LENGTH = 2; - -const handleReasoningStart = (ctx: RenderContext): StreamMode => { - if (!ctx.showReasoning) { - return "none"; - } - writeLine(ctx); - write(ctx, renderReasoningPrefix(ctx)); - ctx.reasoningLineLength = REASONING_PREFIX_LENGTH; - return "reasoning"; -}; - -const handleReasoningDelta = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - if (!ctx.showReasoning) { - return "none"; - } - - const prefix = renderReasoningPrefix(ctx); - const colorSuffix = ctx.useColor - ? `${colors.dim}${colors.italic}${colors.gray}` - : ""; - const colorReset = ctx.useColor ? colors.reset : ""; - const maxWidth = ctx.terminalWidth - 6; - - for (const char of part.text) { - if (char === "\n") { - write(ctx, `${colorReset}\n${prefix}${colorSuffix}`); - ctx.reasoningLineLength = REASONING_PREFIX_LENGTH; - } else if (ctx.reasoningLineLength >= maxWidth) { - write(ctx, `${colorReset}\n${prefix}${colorSuffix}${char}`); - ctx.reasoningLineLength = REASONING_PREFIX_LENGTH + 1; - } else { - write(ctx, char); - ctx.reasoningLineLength++; - } - } - - return "reasoning"; -}; - -const handleReasoningEnd = (ctx: RenderContext): StreamMode => { - if (!ctx.showReasoning) { - return "none"; - } - write(ctx, renderReasoningEnd(ctx)); - writeLine(ctx); - return "none"; -}; - -const handleToolCall = ( - ctx: RenderContext, - part: Extract, - mode: StreamMode -): StreamMode => { - if (mode === "tool-input") { - writeLine(ctx); - } - - const inputState = ctx.activeToolInputs.get(part.toolCallId); - const shouldSkipToolCallRender = - ctx.streamedToolCallIds.has(part.toolCallId) && - inputState?.hasContent === true; - - ctx.activeToolInputs.delete(part.toolCallId); - ctx.streamedToolCallIds.delete(part.toolCallId); - - if (shouldSkipToolCallRender) { - return "none"; - } - - const toolName = ctx.useColor - ? `${colors.bold}${colors.brightYellow}${part.toolName}${colors.reset}` - : part.toolName; - const callId = ctx.useColor - ? `${colors.dim}${colors.gray}(${part.toolCallId})${colors.reset}` - : `(${part.toolCallId})`; - writeLine(ctx, `${renderToolLabel(ctx)} ${toolName} ${callId}`); - const inputLabel = ctx.useColor - ? `${colors.cyan}input:${colors.reset}` - : "input:"; - writeLine(ctx, `${inputLabel} ${formatBlock(part.input)}`); - return "none"; -}; - -const handleToolInputStart = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - const toolCallId = getToolInputId(part); - if (!toolCallId) { - return "none"; - } - - ctx.activeToolInputs.set(toolCallId, { - toolName: part.toolName, - hasContent: false, - }); - ctx.streamedToolCallIds.add(toolCallId); - return "none"; -}; - -const handleToolInputDelta = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - const toolCallId = getToolInputId(part); - if (!toolCallId) { - return "none"; - } - - const state = ctx.activeToolInputs.get(toolCallId); - if (!state) { - ctx.activeToolInputs.set(toolCallId, { - toolName: "tool", - hasContent: false, - }); - } - - const chunk = getToolInputChunk(part); - if (chunk) { - const currentState = ctx.activeToolInputs.get(toolCallId); - if (currentState && !currentState.hasContent) { - writeLine(ctx); - const toolName = ctx.useColor - ? `${colors.bold}${colors.brightYellow}${currentState.toolName}${colors.reset}` - : currentState.toolName; - const callId = ctx.useColor - ? `${colors.dim}${colors.gray}(${toolCallId})${colors.reset}` - : `(${toolCallId})`; - writeLine(ctx, `${renderToolLabel(ctx)} ${toolName} ${callId}`); - const inputLabel = ctx.useColor - ? `${colors.cyan}input:${colors.reset}` - : "input:"; - write(ctx, `${inputLabel} `); - } - - write(ctx, chunk); - if (currentState) { - currentState.hasContent = true; - } - } - - ctx.streamedToolCallIds.add(toolCallId); - return chunk ? "tool-input" : "none"; -}; - -const handleToolInputEnd = ( - ctx: RenderContext, - part: Extract, - mode: StreamMode -): StreamMode => { - const toolCallId = getToolInputId(part); - if (!toolCallId) { - return "none"; - } - - if (mode === "tool-input") { - writeLine(ctx); - } - return "none"; -}; - -const handleToolResult = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - if (!ctx.showToolResults) { - return "none"; - } - writeLine(ctx); - const toolName = ctx.useColor - ? `${colors.bold}${colors.brightYellow}${part.toolName}${colors.reset}` - : part.toolName; - const callId = ctx.useColor - ? `${colors.dim}${colors.gray}(${part.toolCallId})${colors.reset}` - : `(${part.toolCallId})`; - const resultLabel = ctx.useColor - ? `${colors.green}result${colors.reset}` - : "result"; - writeLine(ctx, `${resultLabel} ${toolName} ${callId}`); - const outputLabel = ctx.useColor - ? `${colors.cyan}output:${colors.reset}` - : "output:"; - writeLine(ctx, `${outputLabel} ${formatBlock(part.output)}`); - return "none"; -}; - -const handleToolError = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - writeLine(ctx); - const toolName = ctx.useColor - ? `${colors.bold}${colors.brightYellow}${part.toolName}${colors.reset}` - : part.toolName; - const callId = ctx.useColor - ? `${colors.dim}${colors.gray}(${part.toolCallId})${colors.reset}` - : `(${part.toolCallId})`; - writeLine(ctx, `${renderErrorLabel(ctx)} ${toolName} ${callId}`); - const errorLabel = ctx.useColor - ? `${colors.red}message:${colors.reset}` - : "message:"; - writeLine(ctx, `${errorLabel} ${formatBlock(part.error)}`); - return "none"; -}; - -const handleToolOutputDenied = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - writeLine(ctx); - const toolName = ctx.useColor - ? `${colors.bold}${colors.brightYellow}${part.toolName}${colors.reset}` - : part.toolName; - const callId = ctx.useColor - ? `${colors.dim}${colors.gray}(${part.toolCallId})${colors.reset}` - : `(${part.toolCallId})`; - const deniedLabel = ctx.useColor - ? `${colors.bold}${colors.red}output denied${colors.reset}` - : "output denied"; - writeLine(ctx, `${deniedLabel} ${toolName} ${callId}`); - return "none"; -}; - -const handleStartStep = (ctx: RenderContext): StreamMode => { - if (!ctx.showSteps) { - return "none"; - } - writeLine(ctx); - writeLine(ctx, renderLabel(ctx, "[step start]")); - return "none"; -}; - -const handleFinishStep = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - if (!ctx.showSteps) { - return "none"; - } - writeLine(ctx); - writeLine(ctx, `${renderLabel(ctx, "[step finish]")} ${part.finishReason}`); - return "none"; -}; - -const handleSource = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - if (!ctx.showSources) { - return "none"; - } - writeLine(ctx); - writeLine(ctx, renderLabel(ctx, "[source]")); - writeLine(ctx, formatBlock(part)); - return "none"; -}; - -const handleFile = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - if (!ctx.showFiles) { - return "none"; - } - writeLine(ctx); - writeLine(ctx, renderLabel(ctx, "[file]")); - writeLine(ctx, formatBlock(part.file)); - return "none"; -}; - -const handleFinish = ( - ctx: RenderContext, - part: Extract -): StreamMode => { - if (!ctx.showFinishReason) { - return "none"; - } - writeLine(ctx); - writeLine( - ctx, - `${renderLabel(ctx, "[finish]")} ${part.finishReason ?? "unknown"}` - ); - return "none"; -}; - -export interface ToolApprovalRequestPart { - approvalId: string; - toolCall: { - toolName: string; - toolCallId: string; - input: unknown; - }; - type: "tool-approval-request"; -} - -export const renderFullStream = async ( - stream: AsyncIterable>, - options: StreamRenderOptions = {} -): Promise => { - const ctx: RenderContext = { - output: options.output ?? process.stdout, - showReasoning: options.showReasoning ?? true, - showSteps: options.showSteps ?? false, - showFinishReason: options.showFinishReason ?? env.DEBUG_SHOW_FINISH_REASON, - showToolResults: options.showToolResults ?? env.DEBUG_SHOW_TOOL_RESULTS, - showSources: options.showSources ?? true, - showFiles: options.showFiles ?? true, - useColor: options.useColor ?? Boolean(process.stdout.isTTY), - reasoningLineLength: 0, - terminalWidth: process.stdout.columns || 80, - smoothStream: options.smoothStream ?? true, - smoothDelayMs: options.smoothDelayMs ?? 10, - textBuffer: "", - segmenter: new Intl.Segmenter("ko", { granularity: "word" }), - activeToolInputs: new Map(), - streamedToolCallIds: new Set(), - }; - - let mode: StreamMode = "none"; - - for await (const rawPart of stream) { - const part = rawPart as StreamPart; - switch (part.type) { - case "text-start": - mode = handleTextStart(ctx, mode); - break; - case "text-delta": - mode = await handleTextDelta(ctx, part, mode); - break; - case "text-end": - mode = handleTextEnd(ctx, mode); - break; - case "reasoning-start": - mode = handleReasoningStart(ctx); - break; - case "reasoning-delta": - mode = handleReasoningDelta(ctx, part); - break; - case "reasoning-end": - mode = handleReasoningEnd(ctx); - break; - case "tool-input-start": - mode = handleToolInputStart(ctx, part); - break; - case "tool-input-delta": - mode = handleToolInputDelta(ctx, part); - break; - case "tool-input-end": - mode = handleToolInputEnd(ctx, part, mode); - break; - case "tool-call": - mode = handleToolCall(ctx, part, mode); - break; - case "tool-result": - mode = handleToolResult(ctx, part); - break; - case "tool-error": - mode = handleToolError(ctx, part); - break; - case "tool-output-denied": - mode = handleToolOutputDenied(ctx, part); - break; - case "tool-approval-request": - break; - case "start-step": - mode = handleStartStep(ctx); - break; - case "finish-step": - mode = handleFinishStep(ctx, part); - break; - case "source": - mode = handleSource(ctx, part); - break; - case "file": - mode = handleFile(ctx, part); - break; - case "start": - mode = "none"; - break; - case "finish": - mode = handleFinish(ctx, part); - break; - default: - writeLine(ctx); - writeLine(ctx, renderLabel(ctx, "[unknown part]")); - writeLine(ctx, formatBlock(part)); - mode = "none"; - } - } -}; diff --git a/src/interaction/tool-loop-control.test.ts b/src/interaction/tool-loop-control.test.ts new file mode 100644 index 0000000..5bb0236 --- /dev/null +++ b/src/interaction/tool-loop-control.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "bun:test"; +import { + MANUAL_TOOL_LOOP_MAX_STEPS, + shouldContinueManualToolLoop, +} from "./tool-loop-control"; + +describe("tool loop control", () => { + it("continues when finish reason is tool-calls", () => { + expect(shouldContinueManualToolLoop("tool-calls")).toBe(true); + }); + + it("continues when finish reason is unknown", () => { + expect(shouldContinueManualToolLoop("unknown")).toBe(true); + }); + + it("stops when finish reason is stop", () => { + expect(shouldContinueManualToolLoop("stop")).toBe(false); + }); + + it("uses the expected safety cap", () => { + expect(MANUAL_TOOL_LOOP_MAX_STEPS).toBe(200); + }); +}); diff --git a/src/interaction/tool-loop-control.ts b/src/interaction/tool-loop-control.ts new file mode 100644 index 0000000..af1d405 --- /dev/null +++ b/src/interaction/tool-loop-control.ts @@ -0,0 +1,7 @@ +export const MANUAL_TOOL_LOOP_MAX_STEPS = 200; + +const CONTINUATION_FINISH_REASONS = new Set(["tool-calls", "unknown"]); + +export const shouldContinueManualToolLoop = (finishReason: string): boolean => { + return CONTINUATION_FINISH_REASONS.has(finishReason); +}; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 36b7609..ce1a39e 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,18 +1,31 @@ import type { LanguageModelV3Middleware } from "@ai-sdk/provider"; -import { morphXmlToolMiddleware } from "@ai-sdk-tool/parser"; +import { + hermesToolMiddleware, + morphXmlToolMiddleware, + qwen3CoderToolMiddleware, +} from "@ai-sdk-tool/parser"; +import type { ToolFallbackMode } from "../tool-fallback-mode"; import { trimLeadingNewlinesMiddleware as trimMiddleware } from "./trim-leading-newlines"; export interface MiddlewareOptions { - enableToolFallback: boolean; + toolFallbackMode: ToolFallbackMode; } +const TOOL_FALLBACK_MIDDLEWARES: Readonly< + Record, LanguageModelV3Middleware> +> = { + morphxml: morphXmlToolMiddleware, + hermes: hermesToolMiddleware, + qwen3coder: qwen3CoderToolMiddleware, +}; + export function buildMiddlewares( options: MiddlewareOptions ): LanguageModelV3Middleware[] { const middlewares: LanguageModelV3Middleware[] = [trimMiddleware]; - if (options.enableToolFallback) { - middlewares.push(morphXmlToolMiddleware); + if (options.toolFallbackMode !== "disable") { + middlewares.push(TOOL_FALLBACK_MIDDLEWARES[options.toolFallbackMode]); } return middlewares; diff --git a/src/tool-fallback-mode.ts b/src/tool-fallback-mode.ts new file mode 100644 index 0000000..659df74 --- /dev/null +++ b/src/tool-fallback-mode.ts @@ -0,0 +1,34 @@ +export const TOOL_FALLBACK_MODES = [ + "disable", + "morphxml", + "hermes", + "qwen3coder", +] as const; + +export type ToolFallbackMode = (typeof TOOL_FALLBACK_MODES)[number]; + +export const DEFAULT_TOOL_FALLBACK_MODE: ToolFallbackMode = "disable"; +export const LEGACY_ENABLED_TOOL_FALLBACK_MODE: ToolFallbackMode = "morphxml"; + +const isToolFallbackMode = (value: string): value is ToolFallbackMode => { + return TOOL_FALLBACK_MODES.includes(value as ToolFallbackMode); +}; + +export const parseToolFallbackMode = ( + rawValue: string +): ToolFallbackMode | null => { + const value = rawValue.toLowerCase(); + if (isToolFallbackMode(value)) { + return value; + } + + if (value === "on" || value === "enable" || value === "true") { + return LEGACY_ENABLED_TOOL_FALLBACK_MODE; + } + + if (value === "off" || value === "false") { + return DEFAULT_TOOL_FALLBACK_MODE; + } + + return null; +}; diff --git a/src/tools/execute/format-utils.ts b/src/tools/execute/format-utils.ts index d287a00..82fa29e 100644 --- a/src/tools/execute/format-utils.ts +++ b/src/tools/execute/format-utils.ts @@ -14,7 +14,6 @@ const CEA_WRAPPER_COMMAND_LINE_PATTERN = const TMUX_WAIT_INTERNAL_SUFFIX_PATTERN = /\s*;?\s*tmux\s+wait\s+(?:-S\s+)?cea-[0-9a-z-]+\s*$/i; -const SYSTEM_REMINDER_PREFIX = "[SYSTEM REMINDER]"; const TIMEOUT_PREFIX = "[TIMEOUT]"; const BACKGROUND_PREFIX = "[Background process started]"; @@ -75,10 +74,6 @@ export function formatTerminalScreen(content: string): string { return `${TERMINAL_SCREEN_PREFIX}\n${cleaned}\n${TERMINAL_SCREEN_SUFFIX}`; } -export function formatSystemReminder(message: string): string { - return `${SYSTEM_REMINDER_PREFIX} ${message}`; -} - export interface TimeoutMessageOptions { sessionId?: string; terminalScreen: string; diff --git a/src/tools/execute/noninteractive-wrapper.ts b/src/tools/execute/noninteractive-wrapper.ts index dcb0b89..1126323 100644 --- a/src/tools/execute/noninteractive-wrapper.ts +++ b/src/tools/execute/noninteractive-wrapper.ts @@ -1,6 +1,6 @@ import { platform } from "node:os"; -export interface WrapperResult { +interface WrapperResult { command: string; description: string | null; env: Record; @@ -197,9 +197,10 @@ const TOOL_PATTERNS: ToolPattern[] = [ }, ]; +const WHITESPACE_SPLIT_PATTERN = /\s+/; + function hasFlag(command: string, flag: string): boolean { - const flagPattern = new RegExp(`(^|\\s)${flag}(\\s|$)`); - return flagPattern.test(command); + return command.split(WHITESPACE_SPLIT_PATTERN).includes(flag); } function insertArgsAfterCommand( @@ -273,15 +274,12 @@ export function wrapCommandNonInteractive(command: string): WrapperResult { wrappedCommand = appendArgs(wrappedCommand, tool.suffixArgs); } - const wasModified = - wrappedCommand !== trimmedCommand || Object.keys(env).length > 0; - return { command: wrappedCommand, env, - wrapped: wasModified, + wrapped: true, tool: tool.name, - description: wasModified ? tool.description : null, + description: tool.description, }; } } @@ -307,22 +305,10 @@ export function buildEnvPrefix(env: Record): string { export function getFullWrappedCommand(command: string): string { const result = wrapCommandNonInteractive(command); - if (!result.wrapped) { - return command; - } - const envPrefix = buildEnvPrefix(result.env); return `${envPrefix}${result.command}`; } -export function isLinux(): boolean { - return platform() === "linux"; -} - -export function isDarwin(): boolean { - return platform() === "darwin"; -} - function isWindows(): boolean { return platform() === "win32"; } diff --git a/src/tools/execute/shared-tmux-session.test.ts b/src/tools/execute/shared-tmux-session.test.ts index 8741108..ba6f00a 100644 --- a/src/tools/execute/shared-tmux-session.test.ts +++ b/src/tools/execute/shared-tmux-session.test.ts @@ -1,8 +1,33 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { getToolPath } from "../../utils/tools-manager"; import { cleanupSession, getSharedSession } from "./shared-tmux-session"; const SESSION_ID_PATTERN = /^cea-\d+-[a-z0-9]+$/; const TERMINAL_OUTPUT_PATTERN = /Terminal (Screen|Output)/; +const TMUX_OWNER_PID_KEY = "CEA_OWNER_PID"; +const TMUX_SESSION_PREFIX = "cea-"; +const tmuxPath = getToolPath("tmux") || "tmux"; + +function findUnusedPid(): number { + const start = 1_000_000; + const attempts = 500; + + for (let i = 0; i < attempts; i += 1) { + const candidate = start + i; + try { + process.kill(candidate, 0); + } catch (error) { + // Only treat ESRCH (no such process) as unused; + // EPERM means the process exists but we can't signal it + if ((error as NodeJS.ErrnoException).code === "ESRCH") { + return candidate; + } + } + } + + throw new Error("Unable to find an unused PID for test setup"); +} describe("SharedTmuxSession", () => { beforeAll(() => { @@ -50,6 +75,64 @@ describe("SharedTmuxSession", () => { expect(session.isSessionAlive()).toBe(true); }); + + it("marks created session with owner PID", async () => { + const session = getSharedSession(); + await session.executeCommand("echo owner-tag"); + + const ownerResult = spawnSync( + tmuxPath, + ["show-environment", "-t", session.getSessionId(), TMUX_OWNER_PID_KEY], + { encoding: "utf-8" } + ); + + expect(ownerResult.status).toBe(0); + expect(ownerResult.stdout.trim()).toBe( + `${TMUX_OWNER_PID_KEY}=${process.pid}` + ); + }); + + it("cleans stale owned sessions before creating a new session", async () => { + const staleSessionId = `${TMUX_SESSION_PREFIX}${Date.now()}-stale`; + const deadPid = findUnusedPid(); + + const createResult = spawnSync( + tmuxPath, + ["new-session", "-d", "-s", staleSessionId, "bash +H"], + { encoding: "utf-8" } + ); + expect(createResult.status).toBe(0); + + const ownerResult = spawnSync( + tmuxPath, + [ + "set-environment", + "-t", + staleSessionId, + TMUX_OWNER_PID_KEY, + String(deadPid), + ], + { encoding: "utf-8" } + ); + expect(ownerResult.status).toBe(0); + + try { + cleanupSession(); + const session = getSharedSession(); + await session.executeCommand("echo scavenger"); + + const hasSessionResult = spawnSync( + tmuxPath, + ["has-session", "-t", staleSessionId], + { encoding: "utf-8" } + ); + expect(hasSessionResult.status).not.toBe(0); + } finally { + spawnSync(tmuxPath, ["kill-session", "-t", staleSessionId], { + encoding: "utf-8", + }); + } + }); }); describe("executeCommand", () => { diff --git a/src/tools/execute/shared-tmux-session.ts b/src/tools/execute/shared-tmux-session.ts index 20e1525..02c8bf1 100644 --- a/src/tools/execute/shared-tmux-session.ts +++ b/src/tools/execute/shared-tmux-session.ts @@ -14,6 +14,7 @@ import { } from "./noninteractive-wrapper"; const SESSION_PREFIX = "cea"; +const OWNER_PID_ENV_KEY = "CEA_OWNER_PID"; const DEFAULT_TIMEOUT_MS = 180_000; const BACKGROUND_STARTUP_WAIT_MS = 3000; const SHELL_READY_POLL_MS = 100; @@ -34,13 +35,13 @@ function generateCommandId(): string { return id; } -export interface SendKeysOptions { +interface SendKeysOptions { block?: boolean; maxTimeoutMs?: number; minTimeoutMs?: number; } -export interface ExecuteResult { +interface ExecuteResult { exitCode: number; output: string; } @@ -88,6 +89,7 @@ class SharedTmuxSession { private initialized = false; private destroyed = false; private commandQueue: Promise = Promise.resolve(); + private staleCleanupChecked = false; private constructor() { this.sessionId = process.env.CEA_SESSION_ID || generateSessionId(); @@ -158,6 +160,96 @@ class SharedTmuxSession { }); } + private isProcessAlive(pid: number): boolean { + if (pid <= 0) { + return false; + } + + try { + process.kill(pid, 0); + return true; + } catch (error) { + // EPERM means the process exists but we lack permission to signal it + if ((error as NodeJS.ErrnoException).code === "EPERM") { + return true; + } + return false; + } + } + + private parseOwnerPid(value: string): number | null { + const trimmed = value.trim(); + const prefix = `${OWNER_PID_ENV_KEY}=`; + + if (!trimmed.startsWith(prefix)) { + return null; + } + + const parsed = Number.parseInt(trimmed.slice(prefix.length), 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + + return parsed; + } + + private getSessionOwnerPid(sessionName: string): number | null { + const result = this.execTmuxCommand([ + "show-environment", + "-t", + sessionName, + OWNER_PID_ENV_KEY, + ]); + + if (result.status !== 0) { + return null; + } + + return this.parseOwnerPid(result.stdout || ""); + } + + private cleanupStaleOwnedSessions(): void { + if (this.staleCleanupChecked) { + return; + } + this.staleCleanupChecked = true; + + const listResult = this.execTmuxCommand([ + "list-sessions", + "-F", + "#{session_name}", + ]); + if (listResult.status !== 0) { + return; + } + + const sessions = (listResult.stdout || "") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .filter((line) => line.startsWith(`${SESSION_PREFIX}-`)) + .filter((line) => line !== this.sessionId); + + for (const sessionName of sessions) { + const ownerPid = this.getSessionOwnerPid(sessionName); + if (!ownerPid || this.isProcessAlive(ownerPid)) { + continue; + } + + this.execTmuxCommand(["kill-session", "-t", sessionName]); + } + } + + private markSessionOwnership(): void { + this.execTmuxCommand([ + "set-environment", + "-t", + this.sessionId, + OWNER_PID_ENV_KEY, + String(process.pid), + ]); + } + private execAsync( command: string, timeoutMs: number @@ -221,6 +313,8 @@ class SharedTmuxSession { return; } + this.cleanupStaleOwnedSessions(); + const startCommand = [ "export TERM=xterm-256color", "export SHELL=/bin/bash", @@ -235,6 +329,8 @@ class SharedTmuxSession { throw new Error(`Failed to create tmux session: ${result.stderr}`); } + this.markSessionOwnership(); + this.execSync( `${this.tmuxPath} send-keys -t ${this.sessionId} 'set +H' Enter` ); @@ -769,8 +865,6 @@ class SharedTmuxSession { } } -export const sharedSession = SharedTmuxSession.getInstance(); - export function getSharedSession(): SharedTmuxSession { return SharedTmuxSession.getInstance(); } diff --git a/src/tools/execute/shell-execute.ts b/src/tools/execute/shell-execute.ts index b885423..376ca32 100644 --- a/src/tools/execute/shell-execute.ts +++ b/src/tools/execute/shell-execute.ts @@ -5,21 +5,11 @@ import { getSharedSession } from "./shared-tmux-session"; const MAX_OUTPUT_LENGTH = 50_000; const DEFAULT_TIMEOUT_MS = 2000; -export interface CommandResult { +interface CommandResult { exitCode: number; output: string; } -export class CommandError extends Error { - command: string; - - constructor(message: string, command: string) { - super(message); - this.name = "CommandError"; - this.command = command; - } -} - function truncateOutput(output: string, maxLength: number): string { if (output.length <= maxLength) { return output; diff --git a/src/tools/explore/glob.test.ts b/src/tools/explore/glob.test.ts index 9a42460..2ab35f2 100644 --- a/src/tools/explore/glob.test.ts +++ b/src/tools/explore/glob.test.ts @@ -10,8 +10,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { executeGlob } from "./glob"; -const MTIME_PATTERN = /mtime: \d{4}-\d{2}-\d{2}T/; - describe("executeGlob", () => { let tempDir: string; @@ -59,17 +57,19 @@ describe("executeGlob", () => { }); describe("file listing format", () => { - it("includes mtime for each file", async () => { + it("does not include mtime for each file", async () => { const result = await executeGlob({ pattern: "*.ts", path: tempDir }); - expect(result).toMatch(MTIME_PATTERN); + expect(result).toContain(".ts"); + expect(result).not.toContain(" | mtime: "); }); - it("includes numbered list", async () => { + it("does not include numbered list", async () => { const result = await executeGlob({ pattern: "*.ts", path: tempDir }); - expect(result).toContain(" 1 |"); - expect(result).toContain(" 2 |"); + expect(result).toContain(".ts"); + expect(result).not.toContain(" 1 |"); + expect(result).not.toContain(" 2 |"); }); it("includes full path", async () => { diff --git a/src/tools/explore/glob.ts b/src/tools/explore/glob.ts index 70c905b..7f76c8c 100644 --- a/src/tools/explore/glob.ts +++ b/src/tools/explore/glob.ts @@ -79,10 +79,8 @@ export async function executeGlob({ if (displayFiles.length > 0) { const body = displayFiles - .map((f, i) => { - const num = String(i + 1).padStart(4); - const mtimeStr = f.mtime.toISOString(); - return `${num} | ${f.path} | mtime: ${mtimeStr}`; + .map((f) => { + return f.path; }) .join("\n"); output.push(formatBlock("glob results", body)); @@ -96,7 +94,7 @@ export async function executeGlob({ export const globTool = tool({ description: "Find files by pattern (e.g., '**/*.ts', 'src/**/*.json'). " + - "Returns paths sorted by modification time (newest first) with mtime.", + "Returns paths sorted by modification time (newest first).", inputSchema, execute: executeGlob, }); diff --git a/src/tools/explore/safety-utils.ts b/src/tools/explore/safety-utils.ts index 17d72e1..fd11f76 100644 --- a/src/tools/explore/safety-utils.ts +++ b/src/tools/explore/safety-utils.ts @@ -80,12 +80,12 @@ export async function getIgnoreFilter(): Promise { return ig; } -export function isBinaryFile(path: string): boolean { +function isBinaryFile(path: string): boolean { const ext = path.toLowerCase().slice(path.lastIndexOf(".")); return BINARY_EXTENSIONS.has(ext); } -export interface FileCheckResult { +interface FileCheckResult { allowed: boolean; reason?: string; } @@ -101,9 +101,7 @@ function getPathForIgnoreCheck(filePath: string, cwd: string): string | null { return filePath; } -export async function checkFileReadable( - filePath: string -): Promise { +async function checkFileReadable(filePath: string): Promise { const ig = await getIgnoreFilter(); const pathForIgnoreCheck = getPathForIgnoreCheck(filePath, process.cwd()); @@ -141,55 +139,6 @@ export interface ReadFileOptions { offset?: number; } -export interface ReadFileResult { - content: string; - endLine: number; - startLine: number; - totalLines: number; - truncated: boolean; -} - -export async function safeReadFile( - path: string, - options?: ReadFileOptions -): Promise { - const check = await checkFileReadable(path); - if (!check.allowed) { - throw new Error(check.reason); - } - - const content = await readFile(path, "utf-8"); - const lines = content.split("\n"); - const totalLines = lines.length; - - const offset = options?.offset ?? 0; - const limit = options?.limit ?? MAX_LINES; - - const startLine = Math.min(offset, totalLines); - const endLine = Math.min(startLine + limit, totalLines); - const selectedLines = lines.slice(startLine, endLine); - const truncated = endLine < totalLines; - - return { - content: selectedLines.join("\n"), - totalLines, - startLine, - endLine, - truncated, - }; -} - -export async function shouldIgnorePath(path: string): Promise { - const ig = await getIgnoreFilter(); - return ig.ignores(path); -} - -export function clearIgnoreCache(): void { - cachedIgnore = null; -} - -const WHITESPACE_REGEX = /\s+/; - export function formatNumberedLines( lines: string[], startLine1: number @@ -285,36 +234,3 @@ export async function safeReadFileEnhanced( bytes, }; } - -const ALLOWED_COMMANDS = new Set([ - "node", - "npm", - "pnpm", - "yarn", - "git", - "ls", - "pwd", - "echo", - "cat", - "head", - "tail", - "wc", - "which", - "find", - "type", - "dir", - "ps", - "df", - "du", - "free", - "uname", - "uptime", - "date", - "cal", -]); - -export function isSafeCommand(command: string): boolean { - const tokens = command.trim().split(WHITESPACE_REGEX); - const commandName = tokens[0]; - return ALLOWED_COMMANDS.has(commandName.toLowerCase()); -} diff --git a/src/tools/planning/load-skill.test.ts b/src/tools/planning/load-skill.test.ts index 43d168a..d27b2c7 100644 --- a/src/tools/planning/load-skill.test.ts +++ b/src/tools/planning/load-skill.test.ts @@ -25,6 +25,13 @@ describe("executeLoadSkill", () => { expect(result).toContain("triggers:"); }); + it("loads skill with prompts prefix", async () => { + const result = await executeLoadSkill({ skillName: "prompts:example" }); + + expect(result).toContain("# Skill Loaded: prompts:example"); + expect(result).toContain("# Example Skill"); + }); + it("rejects path traversal attempts with ..", async () => { const result = await executeLoadSkill({ skillName: "../../../etc/passwd" });