From c5b61ee3d8c0ad9f38850f14926cd995c9e5f5c4 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Wed, 13 May 2026 10:18:54 +0300 Subject: [PATCH] shitton --- bun.lock | 179 +- package-lock.json | 1668 ++++------------- package.json | 15 +- src/ai/ai-logger.ts | 283 +++ src/ai/ai-runtime-target.ts | 2 +- src/ai/chat-messages-types.ts | 21 - src/ai/mistral-chat-message.ts | 2 +- src/ai/openai-chat-message.ts | 8 +- src/ai/provider-model-runtime.ts | 29 +- src/ai/provider-request-queue.ts | 22 +- src/ai/speech-to-text.ts | 22 +- src/ai/telegram-stream-message.ts | 7 +- src/ai/text-to-speech.ts | 4 +- src/ai/tool-mappers.ts | 10 +- src/ai/tool-types.ts | 27 +- src/ai/tools/brave-search.ts | 7 +- src/ai/tools/file-system.ts | 8 +- src/ai/tools/market-rates.ts | 4 +- src/ai/tools/python-interpretator.ts | 2 +- src/ai/tools/utils.ts | 2 +- src/ai/tools/weather.ts | 4 +- src/ai/unified-ai-runner.gemini.ts | 156 ++ src/ai/unified-ai-runner.mistral.ts | 137 ++ src/ai/unified-ai-runner.ollama.ts | 404 +++++ src/ai/unified-ai-runner.openai.ts | 426 +++++ src/ai/unified-ai-runner.shared.ts | 1691 +++++++++++++++++ src/ai/unified-ai-runner.tool-ranker.ts | 225 +++ src/ai/unified-ai-runner.ts | 2195 ++--------------------- src/callback_commands/ai-cancel.ts | 4 +- src/commands/ae.ts | 14 +- src/commands/distort.ts | 2 +- src/commands/qr.ts | 2 +- src/common/environment.ts | 4 +- src/common/localization.ts | 4 +- src/model/ollama-request.ts | 3 +- src/util/shell-command-runner.ts | 9 +- src/util/utils.ts | 21 +- tsconfig.json | 24 +- 38 files changed, 3929 insertions(+), 3718 deletions(-) create mode 100644 src/ai/ai-logger.ts create mode 100644 src/ai/unified-ai-runner.gemini.ts create mode 100644 src/ai/unified-ai-runner.mistral.ts create mode 100644 src/ai/unified-ai-runner.ollama.ts create mode 100644 src/ai/unified-ai-runner.openai.ts create mode 100644 src/ai/unified-ai-runner.shared.ts create mode 100644 src/ai/unified-ai-runner.tool-ranker.ts diff --git a/bun.lock b/bun.lock index 060e5e2..c338d01 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,8 @@ "@types/fluent-ffmpeg": "^2.1.28", "@types/node": "^25.6.1", "@types/qrcode": "^1.5.6", - "@typescript-eslint/eslint-plugin": "^8.59.2", - "@typescript-eslint/parser": "^8.59.2", + "@typescript/native-preview": "^7.0.0-beta", "drizzle-kit": "^0.31.10", - "eslint": "^10.3.0", - "tsx": "^4.21.0", - "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2", }, }, }, @@ -99,30 +94,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], - - "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], - - "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], - "@google/genai": ["@google/genai@2.0.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-6XpO+YbGutXkm5QgR7NZktISxSz0dw3pSs9NtCUQwvhJc1eyA3KhdKhE/0Uaxp3a6eul3LC0SKau1bXymjOKUg=="], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -253,14 +226,8 @@ "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/fluent-ffmpeg": ["@types/fluent-ffmpeg@2.1.28", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@25.6.1", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-coJCN8O1q4AGyyqCAUSP06P+SrMTu18BkEj3NVAK07q6QUneD2wzj3CLv9+yP+BMeZQlMvneXqqvDe3w+xcq7g=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], @@ -269,34 +236,24 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260421.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260421.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260421.2" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CmajHI25HpVWE9R1XFoxr+cphJPxoYD3eFioQtAvXYkMFKnLdICMS9pXre9Pybizb75ejRxjKD5/CVG055rEIg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260421.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fHv1r3ZmVo6zxuAIFmuX3w9QxbcauoG0SsWhmDwm6VmRubLlOJIcmTtlmV3JAb9oOnq8LuzZljzT7Q39fSMQDw=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260421.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWTR6xbW9t+JS7D5DQIzo75pqVXVWUxF9PMv/+S6xsnOjCVd6g0ixHcFpFMJMKSUQpGPr8Z5f7b8ks6LHW01jg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BWLQO3nemLDSV5PoE5GPHe1dU9Dth77Kv8/cle9Ujcp4LhPo0KincdPqFH/qKeU/xvW25mgFueflZ1nc4rKuww=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLMEuml3BhUb+jaL0TXQ4xvVODxJF+RhkI+tBWvlynsJI4khTXEiwWh+wPOJrsfBRYFRMXEu28Odl/HXkYze8w=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "x64" }, "sha512-qUrJWTB5/wv4wnRG0TRXElAxc2kykNiRNyEIEqBbLmzDlrcvAW7RRy8MXoY1ZyTiKGMu14itZ3x9oW6+blFpRw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260421.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rc6NsWlZmCs5YUKVzKgwoBOoRUGsPzct4BDMRX0csD1devLBBc4AbUXWKsJRbpwIAnqMO1ld4sNHEb+wXgfNHQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], - - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260421.2", "", { "os": "win32", "cpu": "x64" }, "sha512-GQv1+dya1t6EqF2Cpsb+xoozovdX10JUSf6Kl/8xNkTapzmlHd+uMr+8ku3jIASTxoRGn0Mklgjj3MDKrOTuLg=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -307,13 +264,13 @@ "axios": ["axios@1.16.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w=="], - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], @@ -341,8 +298,6 @@ "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -373,43 +328,11 @@ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@10.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="], - - "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "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=="], - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "fluent-ffmpeg": ["fluent-ffmpeg@2.1.3", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="], @@ -437,12 +360,10 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], "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=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "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=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -461,16 +382,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -479,25 +392,15 @@ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "jsonfile": ["jsonfile@5.0.0", "", { "dependencies": { "universalify": "^0.1.2" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w=="], "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@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -509,14 +412,12 @@ "mime-types": ["mime-types@2.1.29", "", { "dependencies": { "mime-db": "1.46.0" } }, "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "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=="], @@ -525,11 +426,9 @@ "openai": ["openai@6.37.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], @@ -543,20 +442,14 @@ "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=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], "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-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -597,10 +490,6 @@ "systeminformation": ["systeminformation@5.31.6", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "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=="], @@ -609,20 +498,12 @@ "twemoji-parser": ["twemoji-parser@14.0.0", "", {}, "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - - "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], - "typescript-telegram-bot-api": ["typescript-telegram-bot-api@0.16.0", "", { "dependencies": { "axios": "^1.7.7", "form-data": "^4.0.1" } }, "sha512-NaAjXucQiZ87U8La/IMaWDOghbMlJzfMbU4rG8ppFpgPOvEwat/zfN5BM+J2QDvKVGN87qJ+1nELnkm3ctSLnQ=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], @@ -631,8 +512,6 @@ "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "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=="], @@ -645,18 +524,12 @@ "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@esbuild-kit/esm-loader/get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@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=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -673,18 +546,12 @@ "@types/ws/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "bun-types/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "protobufjs/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], @@ -695,7 +562,7 @@ "tsx/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=="], - "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "tsx/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], @@ -753,8 +620,6 @@ "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -808,13 +673,5 @@ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - - "yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/package-lock.json b/package-lock.json index 4ed1767..c1a24c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,13 +31,8 @@ "@types/fluent-ffmpeg": "^2.1.28", "@types/node": "^25.6.1", "@types/qrcode": "^1.5.6", - "@typescript-eslint/eslint-plugin": "^8.59.2", - "@typescript-eslint/parser": "^8.59.2", - "drizzle-kit": "^0.31.10", - "eslint": "^10.3.0", - "tsx": "^4.21.0", - "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2" + "@typescript/native-preview": "^7.0.0-beta", + "drizzle-kit": "^0.31.10" } }, "node_modules/@drizzle-team/brocli": { @@ -935,104 +930,10 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, "node_modules/@google/genai": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.0.0.tgz", - "integrity": "sha512-6XpO+YbGutXkm5QgR7NZktISxSz0dw3pSs9NtCUQwvhJc1eyA3KhdKhE/0Uaxp3a6eul3LC0SKau1bXymjOKUg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.2.0.tgz", + "integrity": "sha512-RA3cuaKLldWTrpxhNm2nAe0Oez/UEuoH11jFjPzbYL7TSP83lMk+ic7pQKZ4ekJdZfnIdgxWl1DwJoUwxmvWGQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1053,72 +954,6 @@ } } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1211,6 +1046,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1227,6 +1065,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1243,6 +1084,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1259,6 +1103,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1275,6 +1122,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1291,6 +1141,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1307,6 +1160,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1323,6 +1179,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1339,6 +1198,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1361,6 +1223,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1383,6 +1248,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1405,6 +1273,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1427,6 +1298,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1449,6 +1323,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1471,6 +1348,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1493,6 +1373,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2098,20 +1981,6 @@ "bun-types": "1.3.13" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/fluent-ffmpeg": { "version": "2.1.28", "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz", @@ -2122,20 +1991,13 @@ "@types/node": "*" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { - "version": "25.6.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.1.tgz", - "integrity": "sha512-coJCN8O1q4AGyyqCAUSP06P+SrMTu18BkEj3NVAK07q6QUneD2wzj3CLv9+yP+BMeZQlMvneXqqvDe3w+xcq7g==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": "~7.21.0" } }, "node_modules/@types/qrcode": { @@ -2163,261 +2025,122 @@ "@types/node": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "node_modules/@typescript/native-preview": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-CmajHI25HpVWE9R1XFoxr+cphJPxoYD3eFioQtAvXYkMFKnLdICMS9pXre9Pybizb75ejRxjKD5/CVG055rEIg==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", "bin": { - "acorn": "bin/acorn" + "tsgo": "bin/tsgo.js" }, - "engines": { - "node": ">=0.4.0" + "optionalDependencies": { + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260421.2", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260421.2", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260421.2", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260421.2", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260421.2", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260421.2", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260421.2" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@typescript/native-preview-darwin-arm64": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-fHv1r3ZmVo6zxuAIFmuX3w9QxbcauoG0SsWhmDwm6VmRubLlOJIcmTtlmV3JAb9oOnq8LuzZljzT7Q39fSMQDw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@typescript/native-preview-darwin-x64": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-KWTR6xbW9t+JS7D5DQIzo75pqVXVWUxF9PMv/+S6xsnOjCVd6g0ixHcFpFMJMKSUQpGPr8Z5f7b8ks6LHW01jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@typescript/native-preview-linux-arm": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-BWLQO3nemLDSV5PoE5GPHe1dU9Dth77Kv8/cle9Ujcp4LhPo0KincdPqFH/qKeU/xvW25mgFueflZ1nc4rKuww==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-linux-arm64": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-VLMEuml3BhUb+jaL0TXQ4xvVODxJF+RhkI+tBWvlynsJI4khTXEiwWh+wPOJrsfBRYFRMXEu28Odl/HXkYze8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-linux-x64": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-qUrJWTB5/wv4wnRG0TRXElAxc2kykNiRNyEIEqBbLmzDlrcvAW7RRy8MXoY1ZyTiKGMu14itZ3x9oW6+blFpRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-win32-arm64": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-Rc6NsWlZmCs5YUKVzKgwoBOoRUGsPzct4BDMRX0csD1devLBBc4AbUXWKsJRbpwIAnqMO1ld4sNHEb+wXgfNHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@typescript/native-preview-win32-x64": { + "version": "7.0.0-dev.20260421.2", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260421.2.tgz", + "integrity": "sha512-GQv1+dya1t6EqF2Cpsb+xoozovdX10JUSf6Kl/8xNkTapzmlHd+uMr+8ku3jIASTxoRGn0Mklgjj3MDKrOTuLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/agent-base": { "version": "7.1.4", @@ -2428,23 +2151,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2491,16 +2197,6 @@ "proxy-from-env": "^2.1.0" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2530,19 +2226,6 @@ "node": "*" } }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2588,6 +2271,17 @@ "node": ">=6" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2618,21 +2312,6 @@ "node": ">= 0.8" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2668,13 +2347,6 @@ "node": ">=0.10.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2968,252 +2640,12 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -3237,57 +2669,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, "node_modules/fluent-ffmpeg": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", @@ -3362,6 +2743,29 @@ "node": ">=12.20.0" } }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3473,19 +2877,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/google-auth-library": { "version": "10.6.2", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", @@ -3582,36 +2973,6 @@ "node": ">= 14" } }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3621,19 +2982,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3655,26 +3003,17 @@ "bignumber.js": "^9.0.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" + "node_modules/jsonfile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", + "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", + "license": "MIT", + "dependencies": { + "universalify": "^0.1.2" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } }, "node_modules/jwa": { "version": "2.0.1", @@ -3697,30 +3036,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/libsql": { "version": "0.5.29", "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", @@ -3753,22 +3068,6 @@ "@libsql/win32-x64-msvc": "0.5.29" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -3805,35 +3104,12 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -3902,56 +3178,6 @@ } } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -3983,29 +3209,6 @@ "node": ">=8" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -4015,16 +3218,6 @@ "node": ">=10.13.0" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/promise-limit": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", @@ -4032,9 +3225,9 @@ "license": "ISC" }, "node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", + "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -4064,16 +3257,6 @@ "node": ">=10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -4091,124 +3274,6 @@ "node": ">=10.13.0" } }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qrcode/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4264,9 +3329,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4334,29 +3399,6 @@ "node": ">=8" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4436,36 +3478,6 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4995,101 +4007,6 @@ "integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==", "license": "MIT" }, - "node_modules/twemoji/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/twemoji/node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/twemoji/node_modules/jsonfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", - "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", - "license": "MIT", - "dependencies": { - "universalify": "^0.1.2" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/twemoji/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/typescript-telegram-bot-api": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/typescript-telegram-bot-api/-/typescript-telegram-bot-api-0.16.0.tgz", @@ -5101,19 +4018,18 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "license": "MIT" }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" } }, "node_modules/web-streams-polyfill": { @@ -5131,42 +4047,30 @@ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5184,19 +4088,99 @@ } } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index a27ffb0..ad80620 100644 --- a/package.json +++ b/package.json @@ -2,20 +2,18 @@ "name": "tg-chat-bot", "main": "src/index.ts", "version": "1.0.0", + "type": "module", "scripts": { - "build": "tsc -p tsconfig.build.json", - "lint": "eslint .", + "build": "tsgo -p tsconfig.json", "start": "node dist/index.js", - "bun:start": "bun run dist/index.js" + "bun:start": "bun run src/index.ts" }, "dependencies": { "@google/genai": "^2.0.0", "@mistralai/mistralai": "^2.2.1", "openai": "^6.37.0", "ollama": "^0.6.3", - "typescript-telegram-bot-api": "^0.16.0", - "@libsql/client": "^0.17.3", "@napi-rs/canvas": "^1.0.0", "axios": "^1.16.0", @@ -34,12 +32,7 @@ "@types/fluent-ffmpeg": "^2.1.28", "@types/node": "^25.6.1", "@types/qrcode": "^1.5.6", - "@typescript-eslint/eslint-plugin": "^8.59.2", - "@typescript-eslint/parser": "^8.59.2", "drizzle-kit": "^0.31.10", - "eslint": "^10.3.0", - "tsx": "^4.21.0", - "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2" + "@typescript/native-preview": "^7.0.0-beta" } } diff --git a/src/ai/ai-logger.ts b/src/ai/ai-logger.ts new file mode 100644 index 0000000..52d4e3b --- /dev/null +++ b/src/ai/ai-logger.ts @@ -0,0 +1,283 @@ +import {Message} from "typescript-telegram-bot-api"; + +export type AiRunnerLogLevel = "trace" | "debug" | "info" | "success" | "warn" | "error"; + +export type AiRunnerLogDetails = Record; + +export type AiLogToolCallLike = { + id: string; + name: string; + argumentsText: string; +}; + +const AI_RUNNER_LOG_PREFIX = "unified-ai-runner"; +const AI_RUNNER_LOG_MAX_STRING = 600; +const AI_RUNNER_LOG_MAX_ARRAY = 8; + +const LOG_LEVEL_WEIGHT: Record = { + trace: 10, + debug: 20, + info: 30, + success: 30, + warn: 40, + error: 50, +}; + +const AI_RUNNER_LOG_COLORS: Record = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + trace: "\x1b[90m", + debug: "\x1b[90m", + info: "\x1b[36m", + success: "\x1b[32m", + warn: "\x1b[33m", + error: "\x1b[31m", + label: "\x1b[35m", + key: "\x1b[94m", + value: "\x1b[97m", +}; + +function envBool(name: string, defaultValue: boolean): boolean { + const value = process.env[name]; + if (value === undefined) return defaultValue; + return !["0", "false", "no", "off"].includes(value.trim().toLowerCase()); +} + +function aiRunnerLogsEnabled(): boolean { + return envBool("AI_RUNNER_LOGS", true) && envBool("AI_LOG_ENABLED", true); +} + +function aiRunnerColorsEnabled(): boolean { + return envBool("AI_RUNNER_LOG_COLORS", true) && !process.env.NO_COLOR; +} + +function configuredMinLevel(): AiRunnerLogLevel { + const raw = process.env.AI_LOG_LEVEL?.trim().toLowerCase(); + if (raw && raw in LOG_LEVEL_WEIGHT) return raw as AiRunnerLogLevel; + return "debug"; +} + +function shouldWriteLevel(level: AiRunnerLogLevel): boolean { + return LOG_LEVEL_WEIGHT[level] >= LOG_LEVEL_WEIGHT[configuredMinLevel()]; +} + +function paintAiLog(value: string, color: keyof typeof AI_RUNNER_LOG_COLORS): string { + if (!aiRunnerColorsEnabled()) return value; + return `${AI_RUNNER_LOG_COLORS[color]}${value}${AI_RUNNER_LOG_COLORS.reset}`; +} + +function truncateAiLogString(value: string, max = AI_RUNNER_LOG_MAX_STRING): string { + if (value.length <= max) return value; + return `${value.slice(0, max)}… (+${value.length - max} chars)`; +} + +function safeJsonParseObject(value?: string): Record { + if (!value?.trim()) return {}; + + try { + const parsed: unknown = JSON.parse(value); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? parsed as Record + : {}; + } catch { + return {}; + } +} + +function isSecretKey(keyPath: string): boolean { + const normalized = keyPath.toLowerCase(); + return normalized.includes("token") + || normalized.includes("secret") + || normalized.includes("apikey") + || normalized.includes("api_key") + || normalized.includes("authorization") + || normalized.endsWith(".key") + || normalized === "key"; +} + +function isPromptKey(keyPath: string): boolean { + const normalized = keyPath.toLowerCase(); + return normalized.includes("prompt") || normalized.includes("systemprompt"); +} + +function isTextPreviewKey(keyPath: string): boolean { + const normalized = keyPath.toLowerCase(); + return normalized.includes("content") + || normalized.includes("message") + || normalized.includes("text") + || normalized.includes("preview") + || normalized.includes("input") + || normalized.includes("output"); +} + +function isToolArgsKey(keyPath: string): boolean { + const normalized = keyPath.toLowerCase(); + return normalized.endsWith("args") + || normalized.endsWith("arguments") + || normalized.includes("toolargs") + || normalized.includes("tool_args"); +} + +function isDaoKey(keyPath: string): boolean { + const normalized = keyPath.toLowerCase(); + return normalized.includes("dao") + || normalized.includes("database") + || normalized.includes("db.") + || normalized.includes("sql") + || normalized.includes("chunk"); +} + +function shouldRedactKey(keyPath: string): boolean { + if (isSecretKey(keyPath)) return true; + if (isPromptKey(keyPath) && !envBool("AI_LOG_PROMPTS", false)) return true; + if (isToolArgsKey(keyPath) && !envBool("AI_LOG_TOOL_ARGS", false)) return true; + if (isDaoKey(keyPath) && !envBool("AI_LOG_DAO", false)) return true; + if (isTextPreviewKey(keyPath) && !envBool("AI_LOG_TEXT_PREVIEW", false)) return true; + return false; +} + +function primitiveToLogValue(value: unknown): unknown { + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack?.split("\n").slice(0, 6).join("\n"), + }; + } + + if (typeof value === "string") return truncateAiLogString(value); + if (typeof value === "number" || typeof value === "boolean" || value === null || value === undefined) return value; + if (typeof value === "bigint") return value.toString(); + if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`; + if (Buffer.isBuffer(value)) return ``; + return undefined; +} + +export function flattenAiLogDetails( + value: unknown, + keyPath = "", + depth = 0, + seen = new WeakSet(), +): Record { + if (keyPath && shouldRedactKey(keyPath)) { + return {[keyPath]: ""}; + } + + const primitive = primitiveToLogValue(value); + if (primitive !== undefined || value === undefined) { + return keyPath ? {[keyPath]: primitive} : {value: primitive}; + } + + if (typeof value !== "object" || value === null) { + return keyPath ? {[keyPath]: String(value)} : {value: String(value)}; + } + + if (seen.has(value)) { + return keyPath ? {[keyPath]: "[Circular]"} : {value: "[Circular]"}; + } + seen.add(value); + + if (Array.isArray(value)) { + if (depth >= 2) { + return keyPath ? {[keyPath]: `[Array ${value.length}]`} : {value: `[Array ${value.length}]`}; + } + + const entries: Record = {}; + value.slice(0, AI_RUNNER_LOG_MAX_ARRAY).forEach((item, index) => { + Object.assign(entries, flattenAiLogDetails(item, keyPath ? `${keyPath}.${index}` : String(index), depth + 1, seen)); + }); + if (value.length > AI_RUNNER_LOG_MAX_ARRAY) { + entries[keyPath ? `${keyPath}.__more` : "__more"] = value.length - AI_RUNNER_LOG_MAX_ARRAY; + } + return entries; + } + + if (depth >= 3) { + return keyPath ? {[keyPath]: "[Object]"} : {value: "[Object]"}; + } + + const entries: Record = {}; + for (const [key, raw] of Object.entries(value as Record)) { + const childPath = keyPath ? `${keyPath}.${key}` : key; + + if ((key.toLowerCase() === "data" || key.toLowerCase() === "image_url" || key.toLowerCase().endsWith("b64")) && typeof raw === "string") { + entries[childPath] = `<${raw.length} chars>`; + continue; + } + + Object.assign(entries, flattenAiLogDetails(raw, childPath, depth + 1, seen)); + } + + return entries; +} + +export function redactLogValue(value: unknown): Record { + return flattenAiLogDetails(value); +} + +function formatAiLogDetails(details?: AiRunnerLogDetails): string { + if (!details || !Object.keys(details).length) return ""; + + const flattened = flattenAiLogDetails(details); + const chunks = Object.entries(flattened).map(([key, value]) => { + const safeValue = typeof value === "string" ? value : JSON.stringify(value); + return `${paintAiLog(key, "key")}=${paintAiLog(safeValue ?? "undefined", "value")}`; + }); + + return ` ${chunks.join(" ")}`; +} + +export function aiLog(level: AiRunnerLogLevel, event: string, details?: AiRunnerLogDetails): void { + if (!aiRunnerLogsEnabled() || !shouldWriteLevel(level)) return; + + const timestamp = paintAiLog(new Date().toISOString(), "dim"); + const prefix = paintAiLog(AI_RUNNER_LOG_PREFIX, "bold"); + const levelText = paintAiLog(level.toUpperCase().padEnd(7), level); + const eventText = paintAiLog(event, "label"); + const line = `${timestamp} ${prefix} ${levelText} ${eventText}${formatAiLogDetails(details)}`; + + if (level === "error") { + console.error(line); + } else if (level === "warn") { + console.warn(line); + } else { + console.log(line); + } +} + +export function aiLogDuration(startedAt: number): string { + const ms = Date.now() - startedAt; + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +export function aiLogToolCall(toolCall: AiLogToolCallLike): Record { + return { + id: toolCall.id, + name: toolCall.name, + arguments: safeJsonParseObject(toolCall.argumentsText), + }; +} + +export function aiLogMessageIdentity(msg: Message | undefined): Record | undefined { + if (!msg) return undefined; + return { + chatId: msg.chat?.id, + chatType: msg.chat?.type, + messageId: msg.message_id, + fromId: msg.from?.id, + username: msg.from?.username, + }; +} + +export function aiLogProviderTarget(target: {provider: string; purpose?: string; model?: string; baseUrl?: string; apiKey?: string} | undefined): Record | undefined { + if (!target) return undefined; + return { + provider: target.provider, + purpose: target.purpose, + model: target.model, + baseUrl: target.baseUrl, + apiKey: target.apiKey, + }; +} diff --git a/src/ai/ai-runtime-target.ts b/src/ai/ai-runtime-target.ts index 8b38542..6d1fa6d 100644 --- a/src/ai/ai-runtime-target.ts +++ b/src/ai/ai-runtime-target.ts @@ -207,7 +207,7 @@ export function createMistralClient(target: AiRuntimeTarget): Mistral { export function createOllamaClient(target: AiRuntimeTarget): Ollama { return new Ollama({ - host: target.baseUrl?.endsWith(":11434") ? target.baseUrl : target.baseUrl + ":11434", + host: target.baseUrl, headers: target.apiKey ? {"Authorization": `Bearer ${target.apiKey}`} : undefined, }); } diff --git a/src/ai/chat-messages-types.ts b/src/ai/chat-messages-types.ts index ba97239..5e6d625 100644 --- a/src/ai/chat-messages-types.ts +++ b/src/ai/chat-messages-types.ts @@ -65,26 +65,5 @@ export function asMistralChatMessage(message: ChatMessage): MistralChatMessage { // } // } -/* - const messages: any[] = ordered.map(part => { - const content: any[] = [{ - type: "input_text", - text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER \"${part.name}\":\n` : "") + part.content, - }]; - - if (!part.bot) { - for (const image of part.images ?? []) { - content.push({type: "input_image", image_url: `data:image/jpeg;base64,${image}`, detail: "auto"}); - } - } - - return {role: part.bot ? "assistant" : "user", content}; - }); - - if (Environment.SYSTEM_PROMPT && Environment.USE_SYSTEM_PROMPT) { - messages.unshift({role: "system", content: Environment.SYSTEM_PROMPT}); - } - return {parts: messages, imageCount}; - */ export type AiChatMessage = | OpenAIChatMessage | OllamaChatMessage | MistralChatMessage | GeminiMessage; diff --git a/src/ai/mistral-chat-message.ts b/src/ai/mistral-chat-message.ts index c767a84..0bf5bdd 100644 --- a/src/ai/mistral-chat-message.ts +++ b/src/ai/mistral-chat-message.ts @@ -67,7 +67,7 @@ export type MistralContentChunk = export type MistralFunctionCall = { name: string; - arguments: { [k: string]: any } | string; + arguments: Record | string; }; export type MistralToolCall = { diff --git a/src/ai/openai-chat-message.ts b/src/ai/openai-chat-message.ts index e96cc59..27f164a 100644 --- a/src/ai/openai-chat-message.ts +++ b/src/ai/openai-chat-message.ts @@ -1,3 +1,7 @@ -import {ResponseInputItem} from "openai/resources/responses/responses"; +import type {ResponseInputMessageContentList} from "openai/resources/responses/responses"; -export type OpenAIChatMessage = ResponseInputItem \ No newline at end of file +export type OpenAIChatMessage = { + type: "message"; + role: "system" | "user" | "assistant"; + content: string | ResponseInputMessageContentList; +}; diff --git a/src/ai/provider-model-runtime.ts b/src/ai/provider-model-runtime.ts index 2a6b772..14cd623 100644 --- a/src/ai/provider-model-runtime.ts +++ b/src/ai/provider-model-runtime.ts @@ -295,26 +295,38 @@ export async function formatRuntimeModelInfo( ); } + +type NamedModel = { + id?: string; + name?: string; + model?: string; +}; + +type ModelListResponse = { + models?: NamedModel[]; + data?: NamedModel[]; +}; + export async function listProviderModels(provider: AiProvider): Promise { const target = resolveAiRuntimeTarget(provider, "chat", getRuntimeModel(provider)); switch (provider) { case AiProvider.OLLAMA: { const ollama = createOllamaClient(target); - const result: any = await ollama.list(); - return (result.models ?? []).map((m: any) => m.model || m.name).filter(Boolean); + const result = await ollama.list() as ModelListResponse; + return (result.models ?? []).map(m => m.model || m.name).filter((name): name is string => !!name); } case AiProvider.GEMINI: { const models: string[] = []; if (getGeminiApiMode(target) === "openai") { const geminiAi = createGeminiOpenAiClient(target); - const iterable: any = await geminiAi.models.list(); + const iterable = await geminiAi.models.list() as AsyncIterable; for await (const model of iterable) models.push(model.name || model.id || String(model)); return models; } const geminiAi = createGoogleGenAiClient(target); - const iterable: any = await geminiAi.models.list(); + const iterable = await geminiAi.models.list() as AsyncIterable; for await (const model of iterable) { const name = model.name || model.id || String(model); models.push(String(name).replace(/^models\//, "")); @@ -323,13 +335,14 @@ export async function listProviderModels(provider: AiProvider): Promise m.id || m.name || String(m)).filter(Boolean); + const result = await mistralAi.models.list() as ModelListResponse | NamedModel[]; + const items = Array.isArray(result) ? result : result.data ?? result.models ?? []; + return items.map(m => m.id || m.name || String(m)).filter((name): name is string => !!name); } case AiProvider.OPENAI: { const openAi = createOpenAiClient(target); - const result: any = await openAi.models.list(); - return (result.data ?? []).map((m: any) => m.id).filter(Boolean); + const result = await openAi.models.list() as ModelListResponse; + return (result.data ?? []).map(m => m.id).filter((id): id is string => !!id); } } } diff --git a/src/ai/provider-request-queue.ts b/src/ai/provider-request-queue.ts index b1394e7..c8f4ea5 100644 --- a/src/ai/provider-request-queue.ts +++ b/src/ai/provider-request-queue.ts @@ -7,11 +7,11 @@ export type AiRequestQueueTarget = { baseUrl?: string; }; -type QueueEntry = { +type QueueEntry = { target: AiRequestQueueTarget; queueKey: string; - run: () => Promise; - resolve: (value: T | PromiseLike) => void; + run: () => Promise; + resolve: (value: unknown) => void; reject: (reason?: unknown) => void; onPositionChange: (requestsBefore: number) => Promise | void; signal?: AbortSignal; @@ -26,7 +26,7 @@ type EnqueueOptions = { }; class AiProviderRequestQueue { - private readonly waiting = new Map>>(); + private readonly waiting = new Map(); private readonly active = new Map(); enqueue(target: AiRequestQueueTarget, options: EnqueueOptions): Promise { @@ -36,11 +36,11 @@ class AiProviderRequestQueue { return new Promise((resolve, reject) => { const queueKey = this.queueKey(target); - const entry: QueueEntry = { + const entry: QueueEntry = { target, queueKey, run: options.run, - resolve, + resolve: value => resolve(value as T), reject, onPositionChange: options.onPositionChange, signal: options.signal, @@ -63,11 +63,11 @@ class AiProviderRequestQueue { }); } - private getQueue(queueKey: string): Array> | undefined { + private getQueue(queueKey: string): QueueEntry[] | undefined { return this.waiting.get(queueKey); } - private getOrCreateQueue(queueKey: string): Array> { + private getOrCreateQueue(queueKey: string): QueueEntry[] { let queue = this.waiting.get(queueKey); if (!queue) { queue = []; @@ -104,7 +104,7 @@ class AiProviderRequestQueue { ]); } - private removeWaitingEntry(entry: QueueEntry): boolean { + private removeWaitingEntry(entry: QueueEntry): boolean { const queue = this.getQueue(entry.queueKey); if (!queue) return false; @@ -147,7 +147,7 @@ class AiProviderRequestQueue { } } - private async runEntry(entry: QueueEntry): Promise { + private async runEntry(entry: QueueEntry): Promise { try { entry.resolve(await entry.run()); } catch (e) { @@ -174,7 +174,7 @@ class AiProviderRequestQueue { }).catch(console.error); } - private deleteQueueIfIdle(queueKey: string, queue: Array>): void { + private deleteQueueIfIdle(queueKey: string, queue: QueueEntry[]): void { if (!queue.length && this.activeCount(queueKey) <= 0) { this.waiting.delete(queueKey); } diff --git a/src/ai/speech-to-text.ts b/src/ai/speech-to-text.ts index 8b1b477..12c8298 100644 --- a/src/ai/speech-to-text.ts +++ b/src/ai/speech-to-text.ts @@ -198,7 +198,7 @@ async function transcribeGeminiSpeech(audio: AiDownloadedFile, signal?: AbortSig temperature: 0, abortSignal: signal, }, - }); + }) as unknown as GeminiSpeechResponse; return { provider: AiProvider.GEMINI, @@ -240,17 +240,19 @@ async function transcribeOllamaSpeech(audio: AiDownloadedFile, signal?: AbortSig }; } -function collectGeminiText(response: any): string { - if (typeof response?.text === "string") return response.text; +type GeminiSpeechResponse = { + text?: string; + candidates?: Array<{content?: {parts?: Array<{text?: string}>}}> ; +}; - const candidates = response?.candidates ?? []; - const candidateText = candidates - .flatMap((candidate: any) => candidate?.content?.parts ?? []) - .map((part: any) => part?.text ?? "") +function collectGeminiText(response: GeminiSpeechResponse): string { + if (typeof response.text === "string") return response.text; + + const candidateText = (response.candidates ?? []) + .flatMap(candidate => candidate.content?.parts ?? []) + .map(part => part.text ?? "") .join(""); if (candidateText.trim()) return candidateText; - return (response?.candidates ?? []) - .map((output: any) => typeof output === "string" ? output : output?.content?.parts?.[0]?.text ?? "") - .join(""); + return ""; } diff --git a/src/ai/telegram-stream-message.ts b/src/ai/telegram-stream-message.ts index 8dc5c6b..2da9717 100644 --- a/src/ai/telegram-stream-message.ts +++ b/src/ai/telegram-stream-message.ts @@ -292,7 +292,7 @@ export class TelegramStreamMessage { } if (shouldRemoveKeyboard) await this.removeKeyboard(); this.lastSent = next; - } catch (e: any) { + } catch (e: unknown) { if (shouldRemoveKeyboard && this.isMessageNotModified(e)) { await this.removeKeyboard(); this.lastSent = next; @@ -369,8 +369,9 @@ export class TelegramStreamMessage { if (result && result !== true) this.waitMessage = result; this.mediaMode = true; this.lastSent = next; - } catch (e: any) { - if (!String(e?.message ?? e).includes("message is not modified")) logError(e); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + if (!message.includes("message is not modified")) logError(e); } } diff --git a/src/ai/text-to-speech.ts b/src/ai/text-to-speech.ts index a56a10b..ad76dc9 100644 --- a/src/ai/text-to-speech.ts +++ b/src/ai/text-to-speech.ts @@ -171,7 +171,7 @@ async function synthesizeMistralSpeech(text: string, voice?: string): Promise { const target = resolveAiRuntimeTarget(AiProvider.GEMINI, "textToSpeech"); const geminiAi = createGoogleGenAiClient(target); - const response: any = await geminiAi.models.generateContent({ + const response = await geminiAi.models.generateContent({ model: target.model, contents: text, config: { diff --git a/src/ai/tool-mappers.ts b/src/ai/tool-mappers.ts index 25b9122..8d5f39d 100644 --- a/src/ai/tool-mappers.ts +++ b/src/ai/tool-mappers.ts @@ -15,7 +15,15 @@ export function getOpenAITools(): AiTool[] { })); } -export function getOpenAIResponsesTools(): any[] { +export type OpenAiResponseTool = { + type: "function"; + name: string; + description?: string; + parameters?: unknown; + strict: false; +}; + +export function getOpenAIResponsesTools(): OpenAiResponseTool[] { return getTools().map(tool => ({ type: "function", name: tool.function.name, diff --git a/src/ai/tool-types.ts b/src/ai/tool-types.ts index 3369282..7905254 100644 --- a/src/ai/tool-types.ts +++ b/src/ai/tool-types.ts @@ -1,28 +1,5 @@ -/* -interface Tool { - type: string; - function: { - name?: string; - description?: string; - type?: string; - parameters?: { - type?: string; - $defs?: any; - items?: any; - required?: string[]; - properties?: { - [key: string]: { - type?: string | string[]; - items?: any; - description?: string; - enum?: any[]; - }; - }; - }; - }; -} - */ + export type AiToolParameters = { type: "object"; @@ -45,7 +22,7 @@ export type AiToolCall = { function: { name: string; arguments: { - [key: string]: any; + [key: string]: unknown; }; }; }; diff --git a/src/ai/tools/brave-search.ts b/src/ai/tools/brave-search.ts index 4aab771..3d64929 100644 --- a/src/ai/tools/brave-search.ts +++ b/src/ai/tools/brave-search.ts @@ -356,11 +356,12 @@ export async function webSearch(args?: Record) { note: "Use returned URLs as sources. Do not invent facts that are not present in the snippets/results.", }; - } catch (e: any) { + } catch (e: unknown) { logError(e); - const status = e?.response?.status; - const data = e?.response?.data; + const axiosLike = e as {response?: {status?: unknown; data?: unknown}}; + const status = axiosLike.response?.status; + const data = axiosLike.response?.data; return { ok: false, diff --git a/src/ai/tools/file-system.ts b/src/ai/tools/file-system.ts index 0ac6d9e..27cf8de 100644 --- a/src/ai/tools/file-system.ts +++ b/src/ai/tools/file-system.ts @@ -327,8 +327,8 @@ async function assertNoSymlinkInPath( if (stat.isSymbolicLink()) { throw new Error("Symlinks are not allowed in file tool paths."); } - } catch (e: any) { - if (e?.code === "ENOENT" && options?.allowMissingTail) { + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === "ENOENT" && options?.allowMissingTail) { return; } @@ -341,8 +341,8 @@ async function pathExists(absolutePath: string): Promise { try { await fs.promises.lstat(absolutePath); return true; - } catch (e: any) { - if (e?.code === "ENOENT") return false; + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") return false; throw e; } } diff --git a/src/ai/tools/market-rates.ts b/src/ai/tools/market-rates.ts index 0f038ec..109f6b9 100644 --- a/src/ai/tools/market-rates.ts +++ b/src/ai/tools/market-rates.ts @@ -58,11 +58,11 @@ export const marketRatesToolPrompt = [ "- When the user asks for all rates, group fiat currencies separately from crypto and gold.", ].join("\n"); -export async function getMarketRates(): Promise { +export async function getMarketRates(): Promise { try { const response = await axios.get("https://apid.r00t.top/api/v2/currency/rates"); return response.data; - } catch (e: any) { + } catch (e: unknown) { console.error("GET_MARKET_RATES", e); return undefined; } diff --git a/src/ai/tools/python-interpretator.ts b/src/ai/tools/python-interpretator.ts index ed9fe70..3f827af 100644 --- a/src/ai/tools/python-interpretator.ts +++ b/src/ai/tools/python-interpretator.ts @@ -692,7 +692,7 @@ function parsePythonInterpreterArgs( return { code, - stdin, + stdin: typeof stdin === "string" ? stdin : undefined, timeoutMs: timeoutMs === undefined ? undefined : Number(timeoutMs), }; } diff --git a/src/ai/tools/utils.ts b/src/ai/tools/utils.ts index d557620..d9a7ed1 100644 --- a/src/ai/tools/utils.ts +++ b/src/ai/tools/utils.ts @@ -96,7 +96,7 @@ export async function loadOllamaModel(model: string, ollama: Ollama, contextLeng } }); return true; - } catch (e: any) { + } catch (e: unknown) { console.error("Error loading Ollama model:", model); return false; } diff --git a/src/ai/tools/weather.ts b/src/ai/tools/weather.ts index c16ea5a..290f7a9 100644 --- a/src/ai/tools/weather.ts +++ b/src/ai/tools/weather.ts @@ -42,7 +42,7 @@ export const weatherToolPrompt = [ "If the city is missing or unclear, ask the user to specify it.", ].join("\n"); -export async function getWeather(args?: Record): Promise { +export async function getWeather(args?: Record): Promise | null> { console.log("getWeather()"); try { const city = asNonEmptyString(args?.city); @@ -137,7 +137,7 @@ export async function getWeather(args?: Record): Promise candidate.content?.parts ?? []) + .map(part => part.text ?? "") + .join(""); +} + +function collectGeminiFunctionCalls(response: GeminiResponseLike): ToolCallData[] { + const calls = response.functionCalls + ?? (response.candidates ?? []).flatMap(candidate => { + return (candidate.content?.parts ?? []) + .map(part => part.functionCall) + .filter((call): call is GeminiFunctionCallLike => !!call); + }); + + return (calls ?? []).map((call, index) => ({ + id: call.id ?? `gemini_${index}_${call.name ?? "call"}`, + name: call.name ?? "", + argumentsText: JSON.stringify(call.args ?? {}), + })).filter((call: ToolCallData) => call.name); +} + +function mergeGeminiFunctionCalls(existing: ToolCallData[], next: ToolCallData[]): ToolCallData[] { + const merged = [...existing]; + for (const call of next) { + const index = merged.findIndex(item => item.id === call.id); + if (index === -1) { + merged.push(call); + } else { + merged[index] = call; + } + } + return merged; +} + +function appendGeminiToolRound(messages: GeminiMessage[], calls: ToolCallData[], results: string[]): void { + messages.push({ + role: "model", + parts: calls.map(call => ({ + functionCall: { + id: call.id, + name: call.name, + args: safeJsonParseObject(call.argumentsText), + }, + })), + }); + + messages.push({ + role: "user", + parts: calls.map((call, index) => ({ + functionResponse: { + id: call.id, + name: call.name, + response: {result: results[index] ?? ""}, + }, + })), + }); +} + +export async function runGemini( + messages: GeminiMessage[], + streamMessage: TelegramStreamMessage, + signal: AbortSignal, + stream: boolean, + firstRoundStatus: string, + config: RuntimeConfigSnapshot, + toolContext: ToolRuntimeContext, +): Promise { + const runnerStartedAt = Date.now(); + const geminiAi = createGoogleGenAiClient(config.geminiChatTarget); + + aiLog("info", "gemini.run.start", { + stream, + target: aiLogProviderTarget(config.geminiChatTarget), + inputMessages: messages.length, + hasToolInputFiles: !!toolContext.pythonInputFiles?.length, + }); + + const toolMemory: ToolExecutionMemory = new Map(); + + for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { + const roundStartedAt = Date.now(); + aiLog("debug", "gemini.round.start", {round, messages: messages.length, stream}); + if (signal.aborted) throw new Error("Aborted"); + + streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? ""); + await streamMessage.flush(); + + const request: GeminiGenerationRequest = { + model: config.geminiChatTarget.model, + contents: messages, + config: { + tools: getGeminiTools(), + temperature: messages.length <= 2 ? 0 : 0.6, + abortSignal: signal, + }, + }; + + if (!stream) { + const response = await geminiAi.models.generateContent(request) as unknown as GeminiResponseLike & { + text?: string + }; + const text = collectGeminiResponseText(response); + streamMessage.append(text); + const calls = collectGeminiFunctionCalls(response); + aiLog(calls.length ? "info" : "success", calls.length ? "gemini.tool_calls" : "gemini.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + textChars: text.length, + calls: calls.map(aiLogToolCall), + }); + if (!calls.length) return; + + appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory)); + continue; + } + + const response = await geminiAi.models.generateContentStream(request) as unknown as AsyncIterableStream; + aiLog("debug", "gemini.stream.open", {round}); + let calls: ToolCallData[] = []; + const roundTextStart = streamMessage.getText().length; + for await (const chunk of response) { + if (signal.aborted) throw new Error("Aborted"); + streamMessage.append(collectGeminiResponseText(chunk)); + calls = mergeGeminiFunctionCalls(calls, collectGeminiFunctionCalls(chunk)); + } + + aiLog(calls.length ? "info" : "success", calls.length ? "gemini.tool_calls" : "gemini.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + textChars: streamMessage.getText().slice(roundTextStart).length, + calls: calls.map(aiLogToolCall), + }); + if (!calls.length) return; + appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory)); + } +} + + +export class GeminiProviderRunner { + static run = runGemini; +} diff --git a/src/ai/unified-ai-runner.mistral.ts b/src/ai/unified-ai-runner.mistral.ts new file mode 100644 index 0000000..95659e9 --- /dev/null +++ b/src/ai/unified-ai-runner.mistral.ts @@ -0,0 +1,137 @@ +// Mistral provider runner extracted from unified-ai-runner.ts. +import {Environment} from "../common/environment"; +import {getMistralTools} from "./tool-mappers"; +import {TelegramStreamMessage} from "./telegram-stream-message"; +import {ToolRuntimeContext} from "./tools/runtime"; +import {MistralChatMessage} from "./mistral-chat-message"; +import {createMistralClient} from "./ai-runtime-target"; +import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "./ai-logger"; + +import {MAX_TOOL_ROUNDS, MistralDeltaLike, MistralDocumentReference, RuntimeConfigSnapshot, StreamingToolCallAccumulator, ToolCallData, ToolExecutionMemory, contentFromMistralDelta, executeToolBatch, mistralToolCalls, normalizeMistralToolCalls, roundStatus} from "./unified-ai-runner.shared"; + +export async function runMistral( + messages: MistralChatMessage[], + documents: MistralDocumentReference[], + streamMessage: TelegramStreamMessage, + signal: AbortSignal, + stream: boolean, + firstRoundStatus: string, + config: RuntimeConfigSnapshot, + toolContext: ToolRuntimeContext, +): Promise { + const runnerStartedAt = Date.now(); + const mistralAi = createMistralClient(config.mistralChatTarget); + aiLog("info", "mistral.run.start", { + stream, + target: aiLogProviderTarget(config.mistralChatTarget), + inputMessages: messages.length, + documents: documents.length, + hasToolInputFiles: !!toolContext.pythonInputFiles?.length, + }); + + const toolMemory: ToolExecutionMemory = new Map(); + + for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { + const roundStartedAt = Date.now(); + aiLog("debug", "mistral.round.start", {round, messages: messages.length, stream}); + if (signal.aborted) throw new Error("Aborted"); + + streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? ""); + await streamMessage.flush(); + + if (!stream) { + const request = { + model: config.mistralChatTarget.model, + messages, + tools: getMistralTools(), + documents: documents + } as unknown as Parameters[0]; + const response = await mistralAi.chat.complete(request, {signal}); + const msg = response.choices?.[0]?.message; + const text = typeof msg?.content === "string" ? msg.content : JSON.stringify(msg?.content ?? ""); + streamMessage.append(text); + const calls = normalizeMistralToolCalls(mistralToolCalls(msg)); + aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + textChars: text.length, + calls: calls.map(aiLogToolCall), + }); + if (!calls.length) return; + messages.push({ + role: "assistant", + content: text, + toolCalls: calls.map(call => ({ + id: call.id, + function: {name: call.name, arguments: call.argumentsText}, + })), + }); + const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory); + for (const [index, call] of calls.entries()) { + messages.push({ + role: "tool", + name: call.name, + toolCallId: call.id, + content: toolResults[index] ?? "", + }); + } + continue; + } + + const request = { + model: config.mistralChatTarget.model, + messages, + tools: getMistralTools(), + documents: documents + } as unknown as Parameters[0]; + const streamResponse = await mistralAi.chat.stream(request, {signal}); + aiLog("debug", "mistral.stream.open", {round}); + let calls: ToolCallData[] = []; + const roundTextStart = streamMessage.getText().length; + const toolCallAccumulator = new StreamingToolCallAccumulator("mistral_stream", round); + + for await (const event of streamResponse) { + if (signal.aborted) throw new Error("Aborted"); + + const choice = event.data?.choices?.[0]; + const delta = choice?.delta; + const mistralDelta = delta as MistralDeltaLike; + + streamMessage.append(contentFromMistralDelta(mistralDelta)); + + const rawDeltaCalls = mistralToolCalls(mistralDelta); + if (rawDeltaCalls.length) { + calls = toolCallAccumulator.add(rawDeltaCalls); + streamMessage.setStatus(Environment.getUseToolText(calls)); + await streamMessage.flush(); + } + } + aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + textChars: streamMessage.getText().slice(roundTextStart).length, + calls: calls.map(aiLogToolCall), + }); + if (!calls.length) return; + const roundText = streamMessage.getText().slice(roundTextStart); + messages.push({ + role: "assistant", + content: roundText, + toolCalls: calls.map(c => ({id: c.id, function: {name: c.name, arguments: c.argumentsText}})) + }); + const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory); + for (const [index, call] of calls.entries()) { + messages.push({ + role: "tool", + name: call.name, + toolCallId: call.id, + content: toolResults[index] ?? "", + }); + } + } +} + + +export class MistralProviderRunner { + static run = runMistral; +} diff --git a/src/ai/unified-ai-runner.ollama.ts b/src/ai/unified-ai-runner.ollama.ts new file mode 100644 index 0000000..43e7895 --- /dev/null +++ b/src/ai/unified-ai-runner.ollama.ts @@ -0,0 +1,404 @@ +// Ollama provider runner extracted from unified-ai-runner.ts. +import {Message} from "typescript-telegram-bot-api"; +import * as fs from "node:fs"; +import path from "node:path"; +import {AiProvider} from "../model/ai-provider"; +import {Environment} from "../common/environment"; +import {bot, notesDir} from "../index"; +import {clamp, logError} from "../util/utils"; +import {getOllamaTools} from "./tool-mappers"; +import {TelegramStreamMessage} from "./telegram-stream-message"; +import {getModelCapabilities} from "./provider-model-runtime"; +import {ChatMessage} from "./chat-messages-types"; +import {ChatRequest, Tool} from "ollama"; +import {ToolRuntimeContext} from "./tools/runtime"; +import {enqueueTelegramApiCall} from "../util/telegram-api-queue"; +import {getCurrentDateTimeTool} from "./tools/datetime"; +import {getMarketRatesTool} from "./tools/market-rates"; +import {getWeatherTool} from "./tools/weather"; +import {loadOllamaModel, unloadAllOllamaModels} from "./tools/utils"; +import {createOllamaClient} from "./ai-runtime-target"; +import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/send-note-file"; +import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "./ai-logger"; + +import {DEFAULT_OLLAMA_CONTEXT_SIZE, MAX_OLLAMA_CONTEXT_SIZE, MAX_TOOL_ROUNDS, MIN_OLLAMA_CONTEXT_SIZE, RuntimeConfigSnapshot, Think, ToolCallData, ToolExecutionMemory, allToolSchemaNames, appendOllamaToolResults, dedupeToolCalls, executeToolBatch, normalizeOllamaToolCalls, roundStatus, safeJsonParseObject, isRecord, isOllamaModelActive, OllamaToolCallLike} from "./unified-ai-runner.shared"; +import {latestUserTextFromOllamaMessages, looksLikeToolRankerJson, OllamaToolRanker} from "./unified-ai-runner.tool-ranker"; + +export async function runOllama( + msg: Message, + messages: ChatMessage[], + streamMessage: TelegramStreamMessage, + signal: AbortSignal, + stream: boolean, + think: Think, + firstRoundStatus: string, + config: RuntimeConfigSnapshot, + toolContext: ToolRuntimeContext, + contextSize?: number, +): Promise { + const fromId = msg.from?.id; + const runnerStartedAt = Date.now(); + + const audioCount = messages.reduce((sum, m) => sum + (m.audioParts?.length || m.audios?.length || 0), 0); + const videoNoteCount = messages.reduce((sum, m) => sum + (m.videoNotes?.length ?? 0), 0); + const imageCount = messages.reduce((sum, m) => sum + (m.imageParts?.length || m.images?.length || 0), 0); + + const target = (audioCount || videoNoteCount) ? config.ollamaAudioTarget : + imageCount ? config.ollamaVisionTarget : + think ? config.ollamaThinkingTarget : config.ollamaChatTarget; + const model = target.model; + aiLog("info", "ollama.run.start", { + stream, + think, + target: aiLogProviderTarget(target), + requestedContextSize: contextSize, + message: aiLogMessageIdentity(msg), + counts: {messages: messages.length, images: imageCount, audio: audioCount, videoNotes: videoNoteCount}, + hasToolInputFiles: !!toolContext.pythonInputFiles?.length, + }); + + const ollama = createOllamaClient(target); + const modelInfo = await ollama.show({model}); + const modelInfoMap = isRecord(modelInfo.model_info) ? modelInfo.model_info : {}; + const contextKey = Object.keys(modelInfoMap).find(k => k.endsWith(".context_length")); + // @ts-ignore + const rawMaxContextLength = contextKey ? modelInfoMap[contextKey] : undefined; + const parsedMaxContextLength = + typeof rawMaxContextLength === "number" + ? rawMaxContextLength + : typeof rawMaxContextLength === "string" + ? Number(rawMaxContextLength) + : DEFAULT_OLLAMA_CONTEXT_SIZE; + + const maxContextLength = Number.isFinite(parsedMaxContextLength) + ? parsedMaxContextLength + : DEFAULT_OLLAMA_CONTEXT_SIZE; + + const context = clamp( + contextSize === -1 ? MAX_OLLAMA_CONTEXT_SIZE : contextSize ?? DEFAULT_OLLAMA_CONTEXT_SIZE, + MIN_OLLAMA_CONTEXT_SIZE, + maxContextLength ?? DEFAULT_OLLAMA_CONTEXT_SIZE + ); + aiLog("debug", "ollama.context.resolved", {model, contextKey, maxContextLength, context}); + + const modelsToLoad = [model]; + + try { + const activeModels = (await ollama.ps()).models.map(m => m.model); + const oldSet = new Set(activeModels); + const newSet = new Set(modelsToLoad); + + const added = modelsToLoad.filter(m => !oldSet.has(m)); + const removed = activeModels.filter(m => !newSet.has(m)); + const diff = [...added, ...removed]; + aiLog("debug", "ollama.models.active", {activeModels, requiredModels: modelsToLoad, added, removed}); + if (diff.length) { + aiLog("info", "ollama.models.unload_extra", {keep: modelsToLoad, diff}); + await unloadAllOllamaModels(ollama, modelsToLoad); + } + } catch (e) { + logError(e); + } + + if (!(await isOllamaModelActive(ollama, target))) { + const loadStartedAt = Date.now(); + aiLog("info", "ollama.model.load.start", {model, context}); + const currentStatus = streamMessage.getStatus(); + streamMessage.setStatus(Environment.getLoadingModelText(model)); + await streamMessage.flush(); + if (await loadOllamaModel(model, ollama, context)) { + aiLog("success", "ollama.model.load.done", {model, duration: aiLogDuration(loadStartedAt)}); + streamMessage.setStatus(currentStatus ?? Environment.waitThinkText); + await streamMessage.flush(); + } + } else { + aiLog("debug", "ollama.model.already_loaded", {model}); + } + + let interval: ReturnType | null = null; + + if (!stream) { + let typingInFlight = false; + const applyTyping = async () => { + if (typingInFlight) return; + typingInFlight = true; + try { + await enqueueTelegramApiCall( + () => bot.sendChatAction({chat_id: msg.chat.id, action: "typing"}), + {method: "sendChatAction", chatId: msg.chat.id, chatType: msg.chat.type} + ).catch(logError); + } finally { + typingInFlight = false; + } + }; + + await applyTyping(); + interval = setInterval(() => { + applyTyping().catch(logError); + }, 5000); + } + + const toolMemory: ToolExecutionMemory = new Map(); + + try { + for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { + const roundStartedAt = Date.now(); + aiLog("debug", "ollama.round.start", { + round, + context, + messages: messages.length, + stream, + think: audioCount ? false : think, + }); + + const request: ChatRequest = { + model: model, + messages: messages, + think: audioCount ? false : think, + options: { + temperature: 0.6, + num_ctx: context, + } + }; + + let activeToolNames: string[] = []; + if ((await getModelCapabilities(AiProvider.OLLAMA, model, "tools"))?.tools?.supported) { + const availableOllamaTools: Tool[] = fromId !== Environment.CREATOR_ID + ? [getCurrentDateTimeTool, getMarketRatesTool, getWeatherTool] + : getOllamaTools() as Tool[]; + + aiLog("debug", "ollama.tools.available", { + round, + tools: allToolSchemaNames(availableOllamaTools), + rankerEnabled: !!config.ollamaToolRankerTarget, + }); + + const rankerSelection = await new OllamaToolRanker(config).selectTools({ + userQuery: latestUserTextFromOllamaMessages(messages), + availableTools: availableOllamaTools, + round, + signal, + }); + + activeToolNames = rankerSelection.selectedNames; + if (rankerSelection.tools.length > 0) { + request.tools = rankerSelection.tools; + } else { + delete request.tools; + } + + aiLog("debug", "ollama.tools.selected", { + round, + tools: activeToolNames, + count: activeToolNames.length, + usedRanker: rankerSelection.usedRanker, + missing: rankerSelection.missing, + }); + } + + if (!stream) { + const response = await ollama.chat({ + ...request, + stream: false + }); + + const message = response.message; + const rawContent = message?.content ?? ""; + + const nativeCalls = dedupeToolCalls( + normalizeOllamaToolCalls( + message?.tool_calls as readonly OllamaToolCallLike[] | undefined, + round, + ), + ); + + const responseText = rawContent; + + if (looksLikeToolRankerJson(responseText)) { + aiLog("error", "ollama.response.looks_like_tool_ranker_json", { + round, + preview: responseText.slice(0, 800), + target: aiLogProviderTarget(target), + }); + throw new Error("Ollama chat model returned tool-ranker JSON. Check that OLLAMA chat target and OLLAMA tools/ranker target are not mixed up."); + } + + streamMessage.append(responseText); + + aiLog("debug", "ollama.response.received", { + round, + duration: aiLogDuration(roundStartedAt), + textChars: responseText.length, + nativeToolCallCount: nativeCalls.length, + }); + + if (!nativeCalls.length) { + aiLog("success", "ollama.run.done", {round, duration: aiLogDuration(runnerStartedAt)}); + break; + } + + const calls = nativeCalls; + + aiLog("info", "ollama.tool_calls", { + round, + calls: calls.map(aiLogToolCall), + }); + + messages.push({ + role: "assistant", + content: responseText, + tool_calls: calls.map(c => ({ + function: { + name: c.name, + arguments: safeJsonParseObject(c.argumentsText), + }, + })), + }); + + appendOllamaToolResults( + messages, + calls, + await executeToolBatch(calls, streamMessage, toolContext, toolMemory), + ); + + continue; + } + + const response = await ollama.chat({ + ...request, + stream: true + }); + + aiLog("debug", "ollama.stream.open", {round}); + const calls: ToolCallData[] = []; + const roundTextStart = streamMessage.getText().length; + const abortOllamaResponse = () => response.abort?.(); + signal.addEventListener("abort", abortOllamaResponse, {once: true}); + if (signal.aborted) abortOllamaResponse(); + try { + for await (const chunk of response) { + const localToolCalls: ToolCallData[] = []; + + localToolCalls.push(...normalizeOllamaToolCalls( + chunk.message.tool_calls as readonly OllamaToolCallLike[] | undefined, + round, + )); + + const newStatus = roundStatus(round, firstRoundStatus, chunk.message.content, localToolCalls, !!chunk.message.thinking); + const previousStatus = streamMessage.getStatus(); + if (newStatus && newStatus !== Environment.waitThinkText) { + streamMessage.setStatus(newStatus); + } else { + streamMessage.clearStatus(); + } + + if (streamMessage.getStatus() !== previousStatus && previousStatus && newStatus !== Environment.waitThinkText) { + await streamMessage.flush(); + } + + if (signal.aborted) { + response.abort?.(); + throw new Error("Aborted"); + } + + if (!(chunk.message?.thinking && streamMessage.getStatus() !== Environment.reasoningText)) { + streamMessage.append(chunk.message?.content ?? ""); + } + + calls.push(...normalizeOllamaToolCalls( + chunk.message?.tool_calls as readonly OllamaToolCallLike[] | undefined, + round, + )); + + if (chunk.done) { + aiLog("debug", "ollama.stream.done", { + round, + duration: aiLogDuration(roundStartedAt), + textChars: streamMessage.getText().slice(roundTextStart).length, + toolCallCount: calls.length, + }); + await streamMessage.flush(streamMessage.regenerateKeyboard(), true); + } + } + } finally { + signal.removeEventListener("abort", abortOllamaResponse); + } + + const streamedRoundText = streamMessage.getText().slice(roundTextStart); + if (!calls.length && looksLikeToolRankerJson(streamedRoundText)) { + streamMessage.replaceText(streamMessage.getText().slice(0, roundTextStart)); + aiLog("error", "ollama.response.looks_like_tool_ranker_json", { + round, + preview: streamedRoundText.slice(0, 800), + target: aiLogProviderTarget(target), + }); + throw new Error("Ollama chat model returned tool-ranker JSON. Check that OLLAMA chat target and OLLAMA tools/ranker target are not mixed up."); + } + + if (!calls.length) { + aiLog("success", "ollama.run.done", { + round, + duration: aiLogDuration(runnerStartedAt), + }); + + break; + } + + calls.splice(0, calls.length, ...dedupeToolCalls(calls)); + + aiLog("info", "ollama.tool_calls", { + round, + calls: calls.map(aiLogToolCall), + }); + + const roundText = streamMessage.getText().slice(roundTextStart); + + messages.push({ + role: "assistant", + content: roundText, + tool_calls: calls.map(c => ({ + function: { + name: c.name, + arguments: safeJsonParseObject(c.argumentsText), + }, + })), + }); + + const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory); + + let successGetNoteFileResult: GetNoteFileResult | undefined = undefined; + + for (const toolResult of toolResults) { + try { + const raw = JSON.parse(toolResult); + const res = GetNoteFileResultSchema.safeParse(raw); + + if (res.success && res.data.success) { + successGetNoteFileResult = res.data; + } + } catch { + // Not every tool result is JSON. + } + } + + if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) { + await bot.sendDocument({ + chat_id: msg.chat.id, + reply_parameters: { + message_id: msg.message_id, + }, + document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)), + }).catch(logError); + } + + appendOllamaToolResults(messages, calls, toolResults); + } + } finally { + if (interval) clearInterval(interval); + } +} + + +export class OllamaProviderRunner { + static run = runOllama; +} diff --git a/src/ai/unified-ai-runner.openai.ts b/src/ai/unified-ai-runner.openai.ts new file mode 100644 index 0000000..cde5da4 --- /dev/null +++ b/src/ai/unified-ai-runner.openai.ts @@ -0,0 +1,426 @@ +// OpenAI and OpenAI-compatible provider runners extracted from unified-ai-runner.ts. +import {Message} from "typescript-telegram-bot-api"; +import {Environment} from "../common/environment"; +import {getOpenAITools} from "./tool-mappers"; +import {TelegramStreamMessage} from "./telegram-stream-message"; +import {ToolRuntimeContext} from "./tools/runtime"; +import {OpenAIChatMessage} from "./openai-chat-message"; +import type {ResponseCreateParamsNonStreaming, ResponseCreateParamsStreaming, ResponseInputItem, ResponseStreamEvent} from "openai/resources/responses/responses"; +import type {ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming} from "openai/resources/chat/completions"; +import {createGeminiOpenAiClient, createOpenAiClient} from "./ai-runtime-target"; +import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "./ai-logger"; + +import {AsyncIterableStream, MAX_TOOL_ROUNDS, OPENAI_IMAGE_PARTIALS, OpenAiChatCompletionResponseLike, OpenAiChatToolCallLike, OpenAiCompatibleChatMessage, OpenAiCompatibleContentPart, OpenAiResponseLike, OpenAiResponseOutputItem, RuntimeConfigSnapshot, ToolCallData, StreamingToolCallAccumulator, collectOpenAiResponseFunctionCalls, collectOpenAiResponseImages, collectOpenAiResponseText, executeToolBatch, getOpenAIResponsesToolsWithImage, openAiResponseItemCallId, safeJsonParseObject, showOpenAiGeneratedImage, ToolExecutionMemory, isRecord, roundStatus, OpenAiChatCompletionStreamChunkLike} from "./unified-ai-runner.shared"; + +export async function runOpenAi( + messages: OpenAIChatMessage[], + streamMessage: TelegramStreamMessage, + signal: AbortSignal, + stream: boolean, + firstRoundStatus: string, + sourceMessage: Message, + config: RuntimeConfigSnapshot, + toolContext: ToolRuntimeContext, +): Promise { + // TODO: 13.05.2026: remove + firstRoundStatus; + const runnerStartedAt = Date.now(); + let responseInput: unknown[] = [...messages]; + const openAi = createOpenAiClient(config.openAiChatTarget); + + aiLog("info", "openai.run.start", { + stream, + target: aiLogProviderTarget(config.openAiChatTarget), + imageTarget: aiLogProviderTarget(config.openAiImageTarget), + inputMessages: messages.length, + sourceMessage: aiLogMessageIdentity(sourceMessage), + hasToolInputFiles: !!toolContext.pythonInputFiles?.length, + }); + + const toolMemory: ToolExecutionMemory = new Map(); + + for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { + const roundStartedAt = Date.now(); + aiLog("debug", "openai.round.start", {round, inputItems: responseInput.length, stream}); + + if (!stream) { + const request: ResponseCreateParamsNonStreaming = { + model: config.openAiChatTarget.model, + input: responseInput as ResponseInputItem[], + // TODO: 13.05.2026, Danil Nikolaev: fix + tools: getOpenAIResponsesToolsWithImage(config) as any, + instructions: config.systemPrompt, + }; + const response = await openAi.responses.create(request, {signal}) as unknown as OpenAiResponseLike; + + const responseText = collectOpenAiResponseText(response); + streamMessage.append(responseText); + aiLog("debug", "openai.response.received", { + round, + duration: aiLogDuration(roundStartedAt), + textChars: responseText.length, + outputItems: response?.output?.length ?? 0, + }); + const images = collectOpenAiResponseImages(response); + if (images.length) { + await showOpenAiGeneratedImage( + streamMessage, + sourceMessage, + images[images.length - 1], + `final_${round}`, + Environment.getImageGenDoneText(config.openAiImageTarget.model), + true, + ); + } + + const calls = collectOpenAiResponseFunctionCalls(response); + aiLog(calls.length ? "info" : "success", calls.length ? "openai.tool_calls" : "openai.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + calls: calls.map(call => ({ + id: call.callId, + name: call.name, + arguments: safeJsonParseObject(call.argumentsText) + })), + }); + if (!calls.length) return; + + const toolCalls = calls.map(call => ({ + id: call.callId, + name: call.name, + argumentsText: call.argumentsText, + })); + const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory); + const toolOutputs = calls.map((call, index) => ({ + type: "function_call_output" as const, + call_id: call.callId, + output: toolResults[index] ?? "", + })); + responseInput = [...responseInput, ...(response.output ?? []), ...toolOutputs]; + continue; + } + + let completedResponse: OpenAiResponseLike | null = null; + const request: ResponseCreateParamsStreaming = { + model: config.openAiChatTarget.model, + input: responseInput as ResponseInputItem[], + stream: true, + // TODO: 13.05.2026, Danil Nikolaev: fix + tools: getOpenAIResponsesToolsWithImage(config) as any, + }; + const response = await openAi.responses.create(request, {signal}) as unknown as AsyncIterableStream; + + aiLog("debug", "openai.stream.open", {round}); + + let localToolCalls: ToolCallData[] = []; + for await (const event of response) { + if (signal.aborted) throw new Error("Aborted"); + + switch (event.type) { + case "response.output_text.delta": + streamMessage.append(event.delta ?? ""); + break; + case "response.image_generation_call.in_progress": + streamMessage.setStatus(Environment.startingImageGenText); + await streamMessage.flush(); + break; + case "response.image_generation_call.generating": + streamMessage.setStatus(Environment.imageGenText); + await streamMessage.flush(); + break; + case "response.image_generation_call.partial_image": { + const iteration = (event.partial_image_index ?? 0) + 1; + await showOpenAiGeneratedImage( + streamMessage, + sourceMessage, + event.partial_image_b64, + `partial_${round}_${iteration}`, + Environment.getPartialImageGenText(iteration, OPENAI_IMAGE_PARTIALS), + false, + ); + break; + } + case "response.image_generation_call.completed": + streamMessage.setStatus(Environment.finalizingImageGenText); + await streamMessage.flush(); + break; + case "response.output_item.added": + if (event.item.type === "function_call" && event.item.name) { + const item = event.item as OpenAiResponseOutputItem & { id?: string }; + localToolCalls.push({ + id: openAiResponseItemCallId(item), + name: item.name ?? "", + argumentsText: item.arguments ?? "{}", + }); + + aiLog("info", "openai.stream.tool_call.added", { + round, + toolCalls: localToolCalls.map(aiLogToolCall) + }); + streamMessage.setStatus(Environment.getUseToolText(localToolCalls)); + await streamMessage.flush(); + } + break; + case "response.output_item.done": + if (event.item.type === "function_call" && event.item.name) { + const item = event.item as OpenAiResponseOutputItem & { id?: string }; + const itemId = openAiResponseItemCallId(item); + const index = localToolCalls.findIndex(c => c.id === itemId); + if (index !== -1) { + localToolCalls.splice(index, 1); + if (localToolCalls.length === 0) { + streamMessage.clearStatus(); + } else { + streamMessage.setStatus(Environment.getUseToolText(localToolCalls)); + } + await streamMessage.flush(); + } + } + break; + case "response.function_call_arguments.delta": + break; + case "response.function_call_arguments.done": + break; + + case "response.completed": + completedResponse = event.response as unknown as OpenAiResponseLike; + break; + case "response.failed": + throw new Error(event.response?.error?.message ?? "OpenAI response failed"); + case "error": + throw new Error(event.message ?? event?.message ?? "OpenAI stream error"); + } + } + + if (!completedResponse) throw new Error("OpenAI did not return the final response.completed event."); + + aiLog("debug", "openai.stream.completed", { + round, + duration: aiLogDuration(roundStartedAt), + outputItems: completedResponse?.output?.length ?? 0, + }); + + const images = collectOpenAiResponseImages(completedResponse); + if (images.length) { + await showOpenAiGeneratedImage( + streamMessage, + sourceMessage, + images[images.length - 1], + `final_${round}`, + Environment.getImageGenDoneText(config.openAiImageTarget.model), + true, + ); + } + + const calls = collectOpenAiResponseFunctionCalls(completedResponse); + aiLog(calls.length ? "info" : "success", calls.length ? "openai.tool_calls" : "openai.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + calls: calls.map(call => ({ + id: call.callId, + name: call.name, + arguments: safeJsonParseObject(call.argumentsText) + })), + }); + if (!calls.length) return; + + const toolCalls = calls.map(call => ({ + id: call.callId, + name: call.name, + argumentsText: call.argumentsText, + })); + const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory); + const toolOutputs = calls.map((call, index) => ({ + type: "function_call_output", + call_id: call.callId, + output: toolResults[index] ?? "", + })); + responseInput = [...responseInput, ...(completedResponse.output ?? []), ...toolOutputs]; + } +} + + +function openAiResponseContentToText(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content.map(part => isRecord(part) ? part.text ?? part.content ?? part.refusal ?? "" : "").join(""); +} + +function openAiResponseMessagesToChatCompletions(messages: OpenAIChatMessage[]): OpenAiCompatibleChatMessage[] { + return messages.map((message): OpenAiCompatibleChatMessage => { + if (message.role === "system" || message.role === "assistant") { + return { + role: message.role, + content: openAiResponseContentToText(message.content), + }; + } + + const content = Array.isArray(message.content) + ? message.content.map((part): OpenAiCompatibleContentPart => { + if (isRecord(part) && part.type === "input_image") { + return { + type: "image_url", + image_url: {url: String(part.image_url ?? "")}, + }; + } + + return { + type: "text", + text: isRecord(part) && typeof part.text === "string" ? part.text : "", + }; + }) + : message.content; + + return {role: "user", content}; + }); +} + +function normalizeOpenAiChatToolCalls(toolCalls: OpenAiChatToolCallLike[] = []): ToolCallData[] { + return toolCalls.map((call, i) => ({ + id: call.id || `openai_chat_${Date.now()}_${i}`, + name: call.function?.name || call.name || "", + argumentsText: typeof call.function?.arguments === "string" + ? call.function.arguments + : JSON.stringify(call.function?.arguments ?? call.arguments ?? {}), + })).filter(call => call.name); +} + +async function appendOpenAiChatToolResults( + messages: OpenAiCompatibleChatMessage[], + calls: ToolCallData[], + results: string[], +): Promise { + for (const [index, call] of calls.entries()) { + messages.push({ + role: "tool", + tool_call_id: call.id, + content: results[index] ?? "", + }); + } +} + +export async function runOpenAiCompatibleChat( + messages: OpenAIChatMessage[], + streamMessage: TelegramStreamMessage, + signal: AbortSignal, + stream: boolean, + firstRoundStatus: string, + config: RuntimeConfigSnapshot, + toolContext: ToolRuntimeContext, +): Promise { + const runnerStartedAt = Date.now(); + const geminiOpenAi = createGeminiOpenAiClient(config.geminiChatTarget); + const chatMessages = openAiResponseMessagesToChatCompletions(messages); + const toolMemory: ToolExecutionMemory = new Map(); + + aiLog("info", "openai_compatible.run.start", { + stream, + target: aiLogProviderTarget(config.geminiChatTarget), + inputMessages: messages.length, + chatMessages: chatMessages.length, + hasToolInputFiles: !!toolContext.pythonInputFiles?.length, + }); + + for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { + const roundStartedAt = Date.now(); + aiLog("debug", "openai_compatible.round.start", {round, messages: chatMessages.length, stream}); + streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? ""); + await streamMessage.flush(); + + if (!stream) { + const request: ChatCompletionCreateParamsNonStreaming = { + model: config.geminiChatTarget.model, + messages: chatMessages, + tools: getOpenAITools(), + temperature: 0.6, + }; + const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as OpenAiChatCompletionResponseLike; + const message = response.choices?.[0]?.message; + streamMessage.append(message?.content ?? ""); + const calls = normalizeOpenAiChatToolCalls(message?.tool_calls ?? []); + aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + textChars: message?.content?.length ?? 0, + calls: calls.map(aiLogToolCall), + }); + if (!calls.length) return; + + chatMessages.push({ + role: "assistant", + content: message?.content ?? "", + tool_calls: calls.map(call => ({ + id: call.id, + type: "function" as const, + function: { + name: call.name, + arguments: call.argumentsText, + }, + })), + }); + await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory)); + continue; + } + + const request: ChatCompletionCreateParamsStreaming = { + model: config.geminiChatTarget.model, + messages: chatMessages, + tools: getOpenAITools(), + temperature: 0.6, + stream: true, + }; + const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as AsyncIterableStream; + + aiLog("debug", "openai_compatible.stream.open", {round}); + // const streamToolCalls: OpenAiChatToolCallLike[] = []; + const roundTextStart = streamMessage.getText().length; + const toolCallAccumulator = new StreamingToolCallAccumulator("openai_chat_stream", round); + let calls: ToolCallData[] = []; + + for await (const chunk of response) { + if (signal.aborted) throw new Error("Aborted"); + + const delta = chunk.choices?.[0]?.delta; + streamMessage.append(delta?.content ?? ""); + + if (delta?.tool_calls?.length) { + calls = toolCallAccumulator.add(delta.tool_calls); + streamMessage.setStatus(Environment.getUseToolText(calls)); + await streamMessage.flush(); + } + } + + // const calls = collectOpenAiChatStreamToolCalls(streamToolCalls); + aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", { + round, + duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt), + textChars: streamMessage.getText().slice(roundTextStart).length, + calls: calls.map(aiLogToolCall), + }); + if (!calls.length) return; + + const roundText = streamMessage.getText().slice(roundTextStart); + chatMessages.push({ + role: "assistant", + content: roundText, + tool_calls: calls.map(call => ({ + id: call.id, + type: "function", + function: { + name: call.name, + arguments: call.argumentsText, + }, + })), + }); + await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory)); + } +} + + +export class OpenAiProviderRunner { + static run = runOpenAi; +} + +export class OpenAiCompatibleProviderRunner { + static run = runOpenAiCompatibleChat; +} diff --git a/src/ai/unified-ai-runner.shared.ts b/src/ai/unified-ai-runner.shared.ts new file mode 100644 index 0000000..22995e4 --- /dev/null +++ b/src/ai/unified-ai-runner.shared.ts @@ -0,0 +1,1691 @@ +import {Message} from "typescript-telegram-bot-api"; +import * as fs from "node:fs"; +import {Blob} from "node:buffer"; +import path from "node:path"; +import {AiProvider} from "../model/ai-provider"; +import {Environment} from "../common/environment"; +import {photoGenDir} from "../index"; +import {collectReplyChainText, delay, logError, replyToMessage} from "../util/utils"; +import {MessageStore} from "../common/message-store"; +import type {OpenAiResponseTool} from "./tool-mappers"; +import {AiProviderName, getOpenAIResponsesTools} from "./tool-mappers"; +import {TelegramArtifactFile, TelegramStreamMessage} from "./telegram-stream-message"; +import {AiDownloadedFile} from "./telegram-attachments"; +import {getRuntimeCapabilities} from "./provider-model-runtime"; +import {StoredAttachment} from "../model/stored-attachment"; +import {AiChatMessage, ChatMessage} from "./chat-messages-types"; +import {ListResponse, Ollama} from "ollama"; +import {executeToolCall, ToolRuntimeContext} from "./tools/runtime"; +import {MessageImagePart, MessagePart} from "../common/message-part"; +import {KeyedAsyncLock} from "../util/async-lock"; +import {type AiRequestQueueTarget} from "./provider-request-queue"; +import {PYTHON_INTERPRETER_TOOL_NAME, pythonInterpreterToolPrompt} from "./tools/python-interpretator"; +import {getResponseLanguageInstruction, UserAiResponseLanguage, UserAiVoiceMode} from "../common/user-ai-settings"; +import { + isTranscribableAudioDownload, + resolveSpeechToTextProviderForUser, + transcribeSpeechDownloads +} from "./speech-to-text"; +import {OpenAIChatMessage} from "./openai-chat-message"; +import type {ResponseInputMessageContentList} from "openai/resources/responses/responses"; +import type {ChatCompletionMessageParam} from "openai/resources/chat/completions"; +import type {GenerateContentParameters} from "@google/genai"; +import {MistralChatMessage} from "./mistral-chat-message"; +import {OllamaChatMessage} from "./ollama-chat-message"; +import {GeminiMessage} from "./gemini-chat-message"; +import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer"; +import {AiRuntimeTarget, createMistralClient, getGeminiApiMode, resolveAiRuntimeTarget} from "./ai-runtime-target"; +import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "./ai-logger"; + +export type {Message} from "typescript-telegram-bot-api"; +export type {AiRuntimeTarget} from "./ai-runtime-target"; +export type {AiDownloadedFile} from "./telegram-attachments"; +export type {StoredAttachment} from "../model/stored-attachment"; +export type {AiChatMessage, ChatMessage} from "./chat-messages-types"; +export type {ToolRuntimeContext} from "./tools/runtime"; +export type {MessageImagePart, MessagePart} from "../common/message-part"; +export type {OpenAIChatMessage} from "./openai-chat-message"; +export type {MistralChatMessage} from "./mistral-chat-message"; +export type {OllamaChatMessage} from "./ollama-chat-message"; +export type {GeminiMessage} from "./gemini-chat-message"; +export type {TelegramArtifactFile} from "./telegram-stream-message"; +export {TelegramStreamMessage} from "./telegram-stream-message"; +export type {ChatRequest, ListResponse, Ollama, Tool} from "ollama"; +export type { + ResponseCreateParamsNonStreaming, + ResponseCreateParamsStreaming, + ResponseInputItem, + ResponseInputMessageContentList, + ResponseStreamEvent, +} from "openai/resources/responses/responses"; +export type { + ChatCompletionCreateParamsNonStreaming, + ChatCompletionCreateParamsStreaming, + ChatCompletionMessageParam, +} from "openai/resources/chat/completions"; +export type {GenerateContentParameters} from "@google/genai"; + +export const TELEGRAM_LIMIT = 4096; +export const MAX_TOOL_ROUNDS = 12; +export const MAX_IDENTICAL_TOOL_CALLS = 1; +export const OPENAI_IMAGE_PARTIALS = 3; +export const AI_REQUEST_TIMEOUT_MS = 10 * 60 * 1000; +export const MIN_OLLAMA_CONTEXT_SIZE = 4096; +export const MAX_OLLAMA_CONTEXT_SIZE = 262144; +export const DEFAULT_OLLAMA_CONTEXT_SIZE = 32768; +export const toolResourceLocks = new KeyedAsyncLock(); + +export type UnifiedRunOptions = { + provider: AiProvider; + msg: Message; + isGuestMsg?: boolean; + text: string; + stream?: boolean; + think?: Think; + responseLanguage?: UserAiResponseLanguage; + contextSize?: number; + voiceMode?: UserAiVoiceMode; + targetMessage?: Message; +}; + +export type ToolCallData = { + id: string; + name: string; + argumentsText: string; +}; + +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; +export type JsonObject = { [key: string]: JsonValue }; + +// SDKs sometimes expose loose object-shaped payloads. Keep the looseness at the boundary, +// but do not spread `unknown` through the rest of the code. +export type LooseRecord = Record; + +export type OpenAiResponsesFunctionCall = { + callId: string; + name: string; + argumentsText: string; +}; + +export type MistralToolCallLike = { + id?: string; + index?: number; + name?: string; + function?: { + name?: string; + arguments?: string | JsonObject; + }; + arguments?: string | JsonObject; +}; + +export type MistralDeltaLike = { + content?: string | Array<{ text?: string }> | null; + toolCalls?: MistralToolCallLike[] | null; + tool_calls?: MistralToolCallLike[] | null; +}; + +export type MistralLibraryLike = { + id?: string; +}; + +export type MistralUploadedDocumentLike = { + id?: string; +}; + +export type MistralDocumentStatusLike = { + processingStatus?: string; +}; + +export type MistralDocumentReference = { + type: "document"; + documentId: string; +}; + +export type OllamaToolCallLike = { + function?: { + name?: string; + arguments?: JsonObject; + }; +}; + +export type OpenAiChatToolCallLike = { + id?: string; + type?: "function"; + index?: number; + name?: string; + function?: { + name?: string; + arguments?: string | JsonObject; + }; + arguments?: string | JsonObject; +}; + +export type OpenAiResponseOutputItem = { + type?: string; + call_id?: string; + name?: string; + arguments?: string; + result?: string; + content?: Array<{ text?: string; refusal?: string }>; +}; + +export type OpenAiResponseLike = { + id?: string; + output?: OpenAiResponseOutputItem[]; + output_text?: string; +}; + +export type GeminiFunctionCallLike = { + id?: string; + name?: string; + args?: JsonObject; +}; + +export type GeminiResponsePartLike = { + text?: string; + functionCall?: GeminiFunctionCallLike; +}; + +export type GeminiResponseLike = { + functionCalls?: GeminiFunctionCallLike[]; + candidates?: Array<{ content?: { parts?: GeminiResponsePartLike[] } }>; +}; + +export type OpenAiCompatibleContentPart = + | { type: "text"; text: string } + | { type: "image_url"; image_url: { url: string } }; + +export type OpenAiCompatibleChatMessage = ChatCompletionMessageParam; + +export type OpenAiChatCompletionResponseLike = { + choices?: Array<{ message?: { content?: string | null; tool_calls?: OpenAiChatToolCallLike[] } }>; +}; + +export type OpenAiChatCompletionStreamChunkLike = { + choices?: Array<{ delta?: { content?: string | null; tool_calls?: OpenAiChatToolCallLike[] } }>; +}; + +export type AsyncIterableStream = AsyncIterable; + +export type GeminiGenerationRequest = GenerateContentParameters; + +export function isRecord(value: unknown): value is LooseRecord { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function toJsonValue(value: unknown): JsonValue | undefined { + if (value === null) return null; + + switch (typeof value) { + case "string": + case "boolean": + return value; + case "number": + return Number.isFinite(value) ? value : null; + case "object": + if (Array.isArray(value)) { + return value.map(item => toJsonValue(item) ?? null); + } + + if (!isRecord(value)) return undefined; + + return Object.fromEntries( + Object.entries(value) + .map(([key, item]) => [key, toJsonValue(item)] as const) + .filter((entry): entry is readonly [string, JsonValue] => entry[1] !== undefined), + ); + default: + return undefined; + } +} + +export function toJsonObject(value: unknown): JsonObject | undefined { + const json = toJsonValue(value); + return json !== null && typeof json === "object" && !Array.isArray(json) ? json : undefined; +} + +export function asOptionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; +} + +export function isAbortError(error: unknown): boolean { + return error instanceof Error ? error.message.includes("Aborted") : String(error).includes("Aborted"); +} + +export type AttachmentKind = "image" | "document" | "audio" | "video" | "video-note"; + +export type Think = boolean | "high" | "medium" | "low"; + +export type RuntimeConfigSnapshot = { + useNamesInPrompt: boolean; + useSystemPrompt: boolean; + systemPrompt?: string; + rankerToolPrompt?: string; + + ollamaChatTarget: AiRuntimeTarget; + ollamaToolRankerTarget?: AiRuntimeTarget; + ollamaVisionTarget: AiRuntimeTarget; + ollamaThinkingTarget: AiRuntimeTarget; + ollamaAudioTarget: AiRuntimeTarget; + ollamaDocumentsTarget: AiRuntimeTarget; + ollamaRagChunkSize: number; + ollamaRagChunkOverlap: number; + ollamaRagTopK: number; + ollamaRagMaxContextChars: number; + ollamaRagMinScore: number; + ollamaRagMaxArchiveFiles: number; + ollamaRagMaxArchiveBytes: number; + ollamaRagMaxArchiveDepth: number; + + geminiChatTarget: AiRuntimeTarget; + + mistralChatTarget: AiRuntimeTarget; + + openAiChatTarget: AiRuntimeTarget; + openAiImageTarget: AiRuntimeTarget; +}; + +export function snapshotRuntimeConfig(): RuntimeConfigSnapshot { + return { + useNamesInPrompt: Environment.USE_NAMES_IN_PROMPT, + useSystemPrompt: Environment.USE_SYSTEM_PROMPT, + + systemPrompt: Environment.SYSTEM_PROMPT, + rankerToolPrompt: Environment.RANKER_TOOL_PROMPT, + + ollamaChatTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat"), + ollamaToolRankerTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "tools"), + ollamaVisionTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "vision"), + ollamaThinkingTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "thinking"), + ollamaAudioTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "audio"), + ollamaDocumentsTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "documents"), + ollamaRagChunkSize: Environment.OLLAMA_RAG_CHUNK_SIZE, + ollamaRagChunkOverlap: Environment.OLLAMA_RAG_CHUNK_OVERLAP, + ollamaRagTopK: Environment.OLLAMA_RAG_TOP_K, + ollamaRagMaxContextChars: Environment.OLLAMA_RAG_MAX_CONTEXT_CHARS, + ollamaRagMinScore: Environment.OLLAMA_RAG_MIN_SCORE, + ollamaRagMaxArchiveFiles: Environment.OLLAMA_RAG_MAX_ARCHIVE_FILES, + ollamaRagMaxArchiveBytes: Environment.OLLAMA_RAG_MAX_ARCHIVE_BYTES, + ollamaRagMaxArchiveDepth: Environment.OLLAMA_RAG_MAX_ARCHIVE_DEPTH, + + geminiChatTarget: resolveAiRuntimeTarget(AiProvider.GEMINI, "chat"), + + mistralChatTarget: resolveAiRuntimeTarget(AiProvider.MISTRAL, "chat"), + + openAiChatTarget: resolveAiRuntimeTarget(AiProvider.OPENAI, "chat"), + openAiImageTarget: resolveAiRuntimeTarget(AiProvider.OPENAI, "outputImages"), + }; +} + +export function getMessageImageParts(part: MessagePart): MessageImagePart[] { + if (part.imageParts?.length) return part.imageParts; + return (part.images ?? []).map(data => ({data, mimeType: "image/jpeg"})); +} + +export function openAiImageDataUrl(image: MessageImagePart): string { + return `data:${image.mimeType || "image/jpeg"};base64,${image.data}`; +} + +export function geminiAudioMimeType(mimeType: string | undefined): string { + const normalized = mimeType?.toLowerCase(); + switch (normalized) { + case "audio/wav": + case "audio/mp3": + case "audio/aiff": + case "audio/aac": + case "audio/ogg": + case "audio/flac": + case "audio/mpeg": + case "audio/m4a": + case "audio/l16": + case "audio/opus": + case "audio/alaw": + case "audio/mulaw": + return normalized; + default: + return "audio/wav"; + } +} + +export function snapshotModel(provider: AiProvider, config: RuntimeConfigSnapshot): string { + switch (provider) { + case AiProvider.OLLAMA: + return config.ollamaChatTarget.model; + case AiProvider.GEMINI: + return config.geminiChatTarget.model; + case AiProvider.MISTRAL: + return config.mistralChatTarget.model; + case AiProvider.OPENAI: + return config.openAiChatTarget.model; + } +} + +export function providerTargets(provider: AiProvider, config: RuntimeConfigSnapshot): AiRuntimeTarget[] { + switch (provider) { + case AiProvider.OLLAMA: + return [ + config.ollamaChatTarget, + config.ollamaToolRankerTarget, + config.ollamaVisionTarget, + config.ollamaThinkingTarget, + config.ollamaAudioTarget, + config.ollamaDocumentsTarget + ].filter((target): target is AiRuntimeTarget => !!target); + case AiProvider.GEMINI: + return [config.geminiChatTarget]; + case AiProvider.MISTRAL: + return [config.mistralChatTarget]; + case AiProvider.OPENAI: + return [config.openAiChatTarget]; + } +} + +export function providerName(provider: AiProvider): AiProviderName { + switch (provider) { + case AiProvider.OLLAMA: + return "ollama"; + case AiProvider.GEMINI: + return "gemini"; + case AiProvider.MISTRAL: + return "mistral"; + case AiProvider.OPENAI: + return "openai"; + } +} + +export function buildSystemInstruction( + config: RuntimeConfigSnapshot, + responseLanguage: UserAiResponseLanguage, + includePythonToolPrompt: boolean, +): string { + return [ + getResponseLanguageInstruction(responseLanguage), + config.systemPrompt && config.useSystemPrompt ? config.systemPrompt : null, + includePythonToolPrompt ? pythonInterpreterToolPrompt : null, + ].filter(Boolean).join("\n\n"); +} + +export function initialStatus(downloads: AiDownloadedFile[], messagePartsImages: number): string { + const documents = downloads.filter(d => d.kind === "document"); + const images = downloads.filter(d => d.kind === "image").length + messagePartsImages; + const audio = downloads.filter(isTranscribableAudioDownload).length; + + if (documents.length) return prepareTelegramMarkdownV2(Environment.getAnalyzingDocumentText(documents.map(d => d.fileName))); + if (audio) return Environment.transcribingAudioText; + if (images > 1) return Environment.analyzingPicturesText; + if (images === 1) return Environment.analyzingPictureText; + return Environment.waitThinkText; +} + +export function hasAudioAttachmentKind(kinds: Set): boolean { + return kinds.has("audio") || kinds.has("video-note"); +} + +export function resolveAiRequestQueueTarget( + options: Pick, + config: RuntimeConfigSnapshot, + requestedAttachmentKinds: Set, +): AiRequestQueueTarget { + switch (options.provider) { + case AiProvider.OLLAMA: + if (hasAudioAttachmentKind(requestedAttachmentKinds)) return config.ollamaAudioTarget; + if (requestedAttachmentKinds.has("image")) return config.ollamaVisionTarget; + return options.think ? config.ollamaThinkingTarget : config.ollamaChatTarget; + case AiProvider.GEMINI: + return config.geminiChatTarget; + case AiProvider.MISTRAL: + return config.mistralChatTarget; + case AiProvider.OPENAI: + return config.openAiChatTarget; + } +} + +export function roundStatus(round: number, firstRoundStatus: string, content?: string, toolCalls?: ToolCallData[], thinking?: boolean): string | null { + if (content?.length && !toolCalls?.length && !thinking) { + // console.log("ROUND_STATUS", "null"); + return null; + } + + const status = toolCalls?.length ? Environment.getUseToolText(toolCalls) + : thinking ? Environment.reasoningText + : round === 0 ? firstRoundStatus + : Environment.waitThinkText; + + // console.log("ROUND_STATUS", status); + + return status; +} + +export function isPlainTextDocument(doc: AiDownloadedFile): boolean { + const ext = path.extname(doc.fileName).toLowerCase(); + const mime = (doc.mimeType ?? "").toLowerCase(); + + return mime.startsWith("text/") + || mime === "application/json" + || mime === "application/xml" + || [ + ".txt", + ".md", + ".markdown", + ".csv", + ".json", + ".jsonl", + ".xml", + ".yaml", + ".yml", + ".ini", + ".env", + ".log", + ".ps1", + ".sh", + ".bat", + ".cmd", + ".js", + ".jsx", + ".ts", + ".tsx", + ".py", + ".rb", + ".go", + ".java", + ".c", + ".cc", + ".cpp", + ".h", + ".hpp", + ".php", + ".sql", + ].includes(ext); +} + +export function decodeTextDocument(doc: AiDownloadedFile): string { + return doc.buffer.toString("utf8").replace(/\u0000/g, ""); +} + +export function downloadedFileAsBlob(doc: AiDownloadedFile): Blob { + const arrayBuffer = doc.buffer.buffer.slice( + doc.buffer.byteOffset, + doc.buffer.byteOffset + doc.buffer.byteLength, + ) as ArrayBuffer; + + return new Blob([arrayBuffer], { + type: doc.mimeType ?? "application/octet-stream", + }); +} + +export function ollamaModelNames(response: ListResponse): string[] { + return (response?.models ?? []) + .flatMap((model) => [model?.model, model?.name]) + .filter((value): value is string => typeof value === "string" && value.length > 0); +} + +export async function isOllamaModelActive(ollama: Ollama, target: AiRuntimeTarget): Promise { + const active = await ollama.ps(); + return ollamaModelNames(active).includes(target.model); +} + +export function addMessageAttachmentKinds(msg: Message | undefined, kinds: Set): void { + if (!msg) return; + + if (msg.photo?.length) kinds.add("image"); + if (msg.document) { + const mimeType = msg.document.mime_type; + kinds.add(mimeType?.startsWith("image/") ? "image" : mimeType?.startsWith("audio/") ? "audio" : "document"); + } + if (msg.voice || msg.audio) kinds.add("audio"); + if (msg.video_note) kinds.add("video-note"); + if (msg.video) kinds.add("video"); +} + +export async function collectStoredReplyChainAttachments(msg: Message): Promise { + const attachments: StoredAttachment[] = []; + const seen = new Set(); + let current = await MessageStore.get(msg.chat.id, msg.message_id); + + for (let i = 0; current && i < 40; i++) { + for (const attachment of current.attachments ?? []) { + const key = [ + attachment.kind, + attachment.fileUniqueId || attachment.fileId, + attachment.cachePath, + ].join(":"); + if (seen.has(key)) continue; + seen.add(key); + attachments.push(attachment); + } + current = await MessageStore.get(current.chatId, current.replyToMessageId); + } + + return attachments; +} + +export async function hasStoredReplyChainImage(msg: Message): Promise { + const attachments = await collectStoredReplyChainAttachments(msg); + if (attachments.some(attachment => attachment.kind === "image")) return true; + + let current = await MessageStore.get(msg.chat.id, msg.message_id); + + for (let i = 0; current && i < 40; i++) { + if (current.photoMaxSizeFilePath?.length) return true; + current = await MessageStore.get(current.chatId, current.replyToMessageId); + } + + return false; +} + +export async function collectRequestedAttachmentKinds(msg: Message): Promise> { + const kinds = new Set(); + + addMessageAttachmentKinds(msg, kinds); + addMessageAttachmentKinds(msg.reply_to_message, kinds); + + for (const attachment of await collectStoredReplyChainAttachments(msg)) { + kinds.add(attachment.kind); + } + + if (!kinds.has("image") && await hasStoredReplyChainImage(msg)) { + kinds.add("image"); + } + + return kinds; +} + +export function unsupportedAttachmentText(provider: AiProvider, model: string, kind: AttachmentKind): string { + const providerName = provider.toLowerCase(); + + switch (kind) { + case "audio": + return Environment.getCurrentModelUnsupportedInputText(model, providerName, "voice or audio messages"); + case "image": + return Environment.getCurrentModelUnsupportedInputText(model, providerName, "images"); + case "document": + return Environment.getCurrentModelUnsupportedInputText(model, providerName, "documents"); + case "video": + return Environment.getCurrentModelUnsupportedInputText(model, providerName, "video"); + case "video-note": + return Environment.getCurrentModelUnsupportedInputText(model, providerName, "video notes"); + } +} + +export async function rejectUnsupportedAttachments( + provider: AiProvider, + model: string, + msg: Message, + config: RuntimeConfigSnapshot, + requestedAttachmentKinds?: Set, +): Promise { + const kinds = requestedAttachmentKinds ?? await collectRequestedAttachmentKinds(msg); + let effectiveModel = model || snapshotModel(provider, config); + const hasAudio = hasAudioAttachmentKind(kinds); + + if (provider === AiProvider.OLLAMA) { + effectiveModel = hasAudio ? config.ollamaAudioTarget.model + : kinds.has("image") ? config.ollamaVisionTarget.model + : config.ollamaChatTarget.model; + } + + const caps = await getRuntimeCapabilities(provider, effectiveModel); + + let speechToTextSupported = !hasAudio; + if (hasAudio && msg.from?.id) { + speechToTextSupported = await resolveSpeechToTextProviderForUser(msg.from.id, provider) + .then(() => true) + .catch(() => false); + } + + const unsupported = + (hasAudio && !speechToTextSupported ? "audio" : null) ?? + (kinds.has("image") && !caps.vision?.supported ? "image" : null) ?? + (kinds.has("document") && !caps.documents?.supported ? "document" : null) ?? + (kinds.has("video") ? "video" : null); + + if (!unsupported) return false; + + if (!kinds.has("audio")) { + await replyToMessage({ + message: msg, + text: unsupportedAttachmentText(provider, effectiveModel, unsupported), + }).catch(logError); + } + + return true; +} + +export async function collectCachedMessageAttachments(msg: Message): Promise<{ + attachments: StoredAttachment[]; + missing: StoredAttachment[] +}> { + const attachments = await collectStoredReplyChainAttachments(msg); + return { + attachments, + missing: attachments.filter(attachment => !fs.existsSync(attachment.cachePath)), + }; +} + +export function safeJsonParseObject(value?: string): JsonObject { + if (!value?.trim()) return {}; + try { + const parsed = JSON.parse(value); + return toJsonObject(parsed) ?? {}; + } catch { + return {}; + } +} + +export type ToolArgumentsParseResult = + | { ok: true; args: JsonObject } + | { ok: false; message: string; raw: string }; + +export function parseToolArgumentsObject(argumentsText?: string): ToolArgumentsParseResult { + const raw = argumentsText ?? ""; + + if (!raw.trim()) { + return {ok: true, args: {}}; + } + + try { + const parsed = JSON.parse(raw); + const args = toJsonObject(parsed); + + if (!args) { + return { + ok: false, + raw, + message: "Tool arguments must be a JSON object.", + }; + } + + return {ok: true, args}; + } catch (error) { + return { + ok: false, + raw, + message: `Invalid JSON in tool arguments: ${error instanceof Error ? error.message : String(error)}`, + }; + } +} + +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export function toolFailureResult(kind: string, message: string, extra?: JsonObject): string { + return JSON.stringify({ + success: false, + error: { + kind, + message, + ...(extra ?? {}), + }, + }); +} + +export function toolRuntimeContextFromDownloads(downloads: AiDownloadedFile[]): ToolRuntimeContext { + if (!downloads.length) return {}; + + return { + pythonInputFiles: downloads.map(download => ({ + kind: download.kind, + path: download.path, + fileName: download.fileName, + mimeType: download.mimeType, + })), + }; +} + +export function extractToolArtifacts(toolName: string, result: string): TelegramArtifactFile[] { + if (toolName !== PYTHON_INTERPRETER_TOOL_NAME) return []; + + try { + const parsed = JSON.parse(result); + const artifacts = isRecord(parsed) && Array.isArray(parsed.artifacts) ? parsed.artifacts : []; + + return artifacts + .map(artifact => artifact as Partial) + .filter((artifact): artifact is TelegramArtifactFile => { + return (artifact.kind === "image" || artifact.kind === "file") + && typeof artifact.path === "string" + && typeof artifact.fileName === "string" + && Number.isSafeInteger(artifact.sizeBytes); + }); + } catch { + return []; + } +} + +export async function sendToolArtifacts(toolCall: ToolCallData, result: string, message: TelegramStreamMessage): Promise { + const artifacts = extractToolArtifacts(toolCall.name, result); + for (const artifact of artifacts) { + await message.sendArtifact(artifact); + } +} + +export function stringifyToolArguments(value: string | JsonObject | undefined): string { + return typeof value === "string" ? value : JSON.stringify(value ?? {}); +} + +export function normalizeMistralToolCalls(calls: MistralToolCallLike[] = []): ToolCallData[] { + return calls.map((call, i) => ({ + id: call.id || `call_${Date.now()}_${i}`, + name: call.function?.name || call.name || "", + argumentsText: stringifyToolArguments(call.function?.arguments ?? call.arguments), + })).filter(c => c.name); +} + +export function mistralToolCalls(value: MistralDeltaLike | { + toolCalls?: MistralToolCallLike[] | null; + tool_calls?: MistralToolCallLike[] | null +} | null | undefined): MistralToolCallLike[] { + return value?.toolCalls ?? value?.tool_calls ?? []; +} + +export function contentFromMistralDelta(delta: MistralDeltaLike): string { + if (!delta.content) return ""; + if (typeof delta.content === "string") return delta.content; + if (Array.isArray(delta.content)) return delta.content.map(c => c.text ?? "").join(""); + return ""; +} + +export function normalizeOllamaToolCalls(calls: readonly OllamaToolCallLike[] = [], round: number): ToolCallData[] { + return calls + .map((call, i) => ({ + id: `ollama_${round}_${i}`, + name: call.function?.name ?? "", + argumentsText: JSON.stringify(call.function?.arguments ?? {}), + })) + .filter(call => !!call.name); +} + +export function buildOpenAiResponseMessage(part: MessagePart, getContent: (part: MessagePart) => string): OpenAIChatMessage { + const content: ResponseInputMessageContentList = [{ + type: "input_text", + text: getContent(part), + }]; + + if (!part.bot) { + for (const image of getMessageImageParts(part)) { + content.push({type: "input_image", image_url: openAiImageDataUrl(image), detail: "auto"}); + } + } + + return {role: part.bot ? "assistant" : "user", content, type: "message"}; +} + +export function buildGeminiMessage(part: MessagePart, getContent: (part: MessagePart) => string): GeminiMessage { + const parts: GeminiMessage["parts"] = [{text: getContent(part)}]; + + if (!part.bot) { + for (const image of getMessageImageParts(part)) { + parts.push({ + inlineData: { + data: image.data, + mimeType: image.mimeType || "image/jpeg", + }, + }); + } + + const audioParts = part.audioParts?.length + ? part.audioParts + : (part.audios ?? []).map(data => ({data, mimeType: "audio/wav"})); + + for (const audio of audioParts) { + parts.push({ + inlineData: { + data: audio.data, + mimeType: geminiAudioMimeType(audio.mimeType), + }, + }); + } + + for (const videoNote of part.videoNotes ?? []) { + parts.push({ + inlineData: { + data: videoNote, + mimeType: "audio/wav", + }, + }); + } + } + + return { + role: part.bot ? "model" : "user", + parts, + }; +} + +export async function collectTextMessages( + msg: Message, + textOverride: string, + provider: AiProvider, + downloads: AiDownloadedFile[], + config: RuntimeConfigSnapshot, + responseLanguage: UserAiResponseLanguage, +): Promise<{ + chatMessages: AiChatMessage[]; + imageCount: number +}> { + const storedMsg = await MessageStore.get(msg.chat.id, msg.message_id); + const messageParts = await collectReplyChainText({triggerMsg: storedMsg, downloads: downloads}); + + const cleanTextOverride = textOverride?.trim(); + if (messageParts.length && cleanTextOverride) { + const latest = messageParts[0]; + if (!latest.bot) latest.content = textOverride.trim(); + } + + const ordered = messageParts.reverse(); + const imageCount = ordered.reduce((sum, p) => sum + (p.bot ? 0 : getMessageImageParts(p).length), 0); + const includePythonToolPrompt = Environment.ENABLE_PYTHON_INTERPRETER && msg.from?.id === Environment.CREATOR_ID; + const systemInstruction = buildSystemInstruction(config, responseLanguage, includePythonToolPrompt); + + const getContent = (part: MessagePart): string => { + if (part.bot) return part.content; + + const userInfo = [ + "[user_info]:", + `name: ${part.name}`, + `username: @${part.userName}`, + "" + ].join("\n"); + + const finalContent = [ + part.content + ]; + + if (Environment.USE_NAMES_IN_PROMPT) { + finalContent.unshift(userInfo); + } + + return finalContent.join("\n"); + }; + + if (provider === AiProvider.OPENAI) { + const messages: OpenAIChatMessage[] = ordered.map(part => buildOpenAiResponseMessage(part, getContent)); + + if (systemInstruction) { + messages.unshift({role: "system", content: systemInstruction, type: "message"}); + } + return {chatMessages: messages, imageCount}; + } + + if (provider === AiProvider.MISTRAL) { + const messages: MistralChatMessage[] = ordered.map(part => { + if (part.bot) { + return { + role: "assistant", + content: [{type: "text", text: getContent(part)}] + }; + } else { + return { + role: "user", + content: [ + {type: "text", text: getContent(part)}, + ...getMessageImageParts(part).map(p => { + return { + type: "image_url" as const, + imageUrl: `data:${p.mimeType || "image/jpeg"};base64,${p.data}` + }; + }) + ] + }; + } + }); + + if (systemInstruction) { + messages.unshift({role: "system", content: systemInstruction}); + } + return {chatMessages: messages, imageCount}; + } + + if (provider === AiProvider.OLLAMA) { + const messages: OllamaChatMessage[] = ordered.map(part => ({ + role: part.bot ? "assistant" : "user", + content: getContent(part), + images: part.bot ? undefined : part.images, + imageParts: part.imageParts, + audios: part.audios, + audioParts: part.audioParts, + videos: part.videos, + videoNotes: part.videoNotes + })); + + if (systemInstruction) { + messages.unshift({ + role: "system", + content: systemInstruction + }); + } + + return {chatMessages: messages, imageCount}; + } + + if (provider === AiProvider.GEMINI) { + if (getGeminiApiMode(config.geminiChatTarget) === "openai") { + const messages: OpenAIChatMessage[] = ordered.map(part => buildOpenAiResponseMessage(part, getContent)); + if (systemInstruction) { + messages.unshift({role: "system", content: systemInstruction, type: "message"}); + } + + return {chatMessages: messages, imageCount}; + } + + const messages: GeminiMessage[] = ordered.map(part => buildGeminiMessage(part, getContent)); + if (systemInstruction) { + messages.unshift({ + role: "user", + parts: [{text: systemInstruction}], + }); + } + + return {chatMessages: messages, imageCount}; + } + + return {chatMessages: [], imageCount: -1}; +} + +export async function transcribeAudioIfNeeded(provider: AiProvider, userId: number | undefined, downloads: AiDownloadedFile[], message: TelegramStreamMessage, signal: AbortSignal): Promise { + const audioDownloads = downloads.filter(isTranscribableAudioDownload); + if (!audioDownloads.length) return ""; + if (!userId) throw new Error(Environment.couldNotIdentifyUserForSpeechToTextText); + if (signal.aborted) throw new Error("Aborted"); + + const startedAt = Date.now(); + aiLog("info", "speech_to_text.start", { + requestedProvider: providerName(provider), + userId, + files: audioDownloads.map(d => ({ + kind: d.kind, + fileName: d.fileName, + mimeType: d.mimeType, + sizeBytes: d.buffer.length + })), + }); + + message.setStatus(Environment.transcribingAudioText); + await message.flush(); + + try { + const resolved = await resolveSpeechToTextProviderForUser(userId, provider); + aiLog("debug", "speech_to_text.provider_resolved", {provider: String(resolved.provider)}); + + const transcript = await transcribeSpeechDownloads(resolved.provider, downloads, signal); + if (!transcript.trim()) { + throw new Error(Environment.speechToTextEmptyResultText); + } + + aiLog("success", "speech_to_text.done", { + duration: aiLogDuration(startedAt), + transcriptChars: transcript.length, + }); + return transcript; + } catch (e) { + aiLog("error", "speech_to_text.failed", {duration: aiLogDuration(startedAt), error: e}); + throw e; + } +} + +export function stripAudioFromRunnerMessages(parts: AiChatMessage[]): void { + for (const part of parts) { + if ("audios" in part) { + delete part.audios; + } + if ("audioParts" in part) { + delete part.audioParts; + } + + if ("videoNotes" in part) { + delete part.videoNotes; + } + + if ("parts" in part && Array.isArray(part.parts)) { + part.parts = part.parts.filter(geminiPart => { + if (!("inlineData" in geminiPart)) return true; + const mimeType = geminiPart.inlineData.mimeType.toLowerCase(); + return !mimeType.startsWith("audio/") && !mimeType.startsWith("video/"); + }); + } + } +} + +export function appendTranscriptToChatMessages( + chatMessages: AiChatMessage[], + provider: AiProvider, + transcript: string, +): void { + const lastUser = [...chatMessages].reverse().find(message => "role" in message && message.role === "user"); + if (!lastUser) return; + + const text = transcript.trim(); + if (!text) return; + + if (provider === AiProvider.GEMINI && "parts" in lastUser && Array.isArray(lastUser.parts)) { + lastUser.parts.push({text}); + return; + } + + if (!("content" in lastUser)) return; + + if (typeof lastUser.content === "string") { + lastUser.content = [lastUser.content, text].filter((value: string) => value.trim()).join("\n\n"); + return; + } + + if (Array.isArray(lastUser.content)) { + const usesOpenAiResponsesParts = lastUser.content.some(part => { + if (!isRecord(part)) return false; + + // Do not read `part.type` directly here: for some providers TypeScript + // narrows it to the Chat Completions union (`text | image_url | thinking`), + // which makes comparisons with Responses parts (`input_text | input_image`) + // look impossible even though this is a runtime mixed-provider guard. + const record: Record = part; + const partType = record["type"]; + + return partType === "input_text" || partType === "input_image"; + }); + + lastUser.content.push( + (usesOpenAiResponsesParts + ? {type: "input_text", text} + : {type: "text", text}) as any, + ); + } +} + +export async function deleteMistralLibrary(libraryId: string | undefined, target: AiRuntimeTarget): Promise { + if (!libraryId) return; + + const startedAt = Date.now(); + aiLog("debug", "mistral.library.delete.start", {libraryId, target: aiLogProviderTarget(target)}); + try { + const mistralAi = createMistralClient(target); + await mistralAi.beta.libraries.delete({libraryId}); + aiLog("success", "mistral.library.delete.done", {libraryId, duration: aiLogDuration(startedAt)}); + } catch (e) { + aiLog("error", "mistral.library.delete.failed", {libraryId, duration: aiLogDuration(startedAt), error: e}); + logError(e); + } +} + +export async function appendMistralTextDocument(doc: AiDownloadedFile, messages: MistralChatMessage[], message: TelegramStreamMessage): Promise { + const startedAt = Date.now(); + aiLog("info", "mistral.document.text.start", { + fileName: doc.fileName, + mimeType: doc.mimeType, + sizeBytes: doc.buffer.length, + }); + + message.setStatus(prepareTelegramMarkdownV2(Environment.getAnalyzingDocumentText([doc.fileName]))); + await message.flush(); + + const text = decodeTextDocument(doc).trim(); + if (!text) { + throw new Error(Environment.getDocumentIsEmptyText(doc.fileName)); + } + + messages.push({ + role: "user", + content: [ + { + type: "text", + text: Environment.getDocumentContentText(doc.fileName, text), + }, + ], + }); + + aiLog("success", "mistral.document.text.done", { + fileName: doc.fileName, + duration: aiLogDuration(startedAt), + chars: text.length, + }); +} + +export async function prepareMistralDocuments(downloads: AiDownloadedFile[], messages: MistralChatMessage[], message: TelegramStreamMessage, target: AiRuntimeTarget, signal: AbortSignal): Promise<{ + documents: MistralDocumentReference[]; + libraryId?: string +}> { + const docs = downloads.filter(d => d.kind === "document"); + const result: MistralDocumentReference[] = []; + if (!docs.length) return {documents: result}; + + const startedAt = Date.now(); + aiLog("info", "mistral.documents.prepare.start", { + target: aiLogProviderTarget(target), + count: docs.length, + documents: docs.map(d => ({fileName: d.fileName, mimeType: d.mimeType, sizeBytes: d.buffer.length})), + }); + + const mistralAi = createMistralClient(target); + const library = await mistralAi.beta.libraries.create({ + name: `tg-chat-bot-${Date.now()}`, + description: "Temporary library for document search", + }, {signal}); + const libraryId = (library as MistralLibraryLike).id; + if (!libraryId) { + throw new Error(Environment.mistralLibraryIdMissingText); + } + + aiLog("debug", "mistral.library.created", {libraryId}); + + try { + for (const doc of docs) { + if (signal.aborted) throw new Error("Aborted"); + + if (isPlainTextDocument(doc)) { + await appendMistralTextDocument(doc, messages, message); + continue; + } + + message.setStatus(prepareTelegramMarkdownV2(Environment.getAnalyzingDocumentText([doc.fileName]))); + await message.flush(); + + const documentStartedAt = Date.now(); + aiLog("info", "mistral.document.upload.start", { + libraryId, + fileName: doc.fileName, + mimeType: doc.mimeType, + sizeBytes: doc.buffer.length, + }); + + const uploaded = await mistralAi.beta.libraries.documents.upload({ + libraryId, + requestBody: { + file: downloadedFileAsBlob(doc), + }, + }, {signal}); + + const uploadedDocument = uploaded as MistralUploadedDocumentLike & { document_id?: string }; + const documentId = uploadedDocument.id ?? uploadedDocument.document_id; + if (!documentId) { + throw new Error(Environment.getMistralUploadedDocumentIdMissingText(doc.fileName)); + } + + aiLog("debug", "mistral.document.upload.done", { + libraryId, + documentId, + fileName: doc.fileName, + duration: aiLogDuration(documentStartedAt), + }); + + let processed = false; + for (let i = 0; i < 90; i++) { + const info = await mistralAi.beta.libraries.documents.status({libraryId, documentId}, {signal}); + const statusInfo = info as MistralDocumentStatusLike & { status?: string; process_status?: string }; + const status = statusInfo.status ?? statusInfo.process_status ?? statusInfo.processingStatus; + aiLog("debug", "mistral.document.status", { + libraryId, + documentId, + fileName: doc.fileName, + status, + attempt: i + 1 + }); + + if (status === "processed" || status === "Completed" || status === "done" || status === "Done") { + processed = true; + break; + } + if (status === "failed" || status === "error" || status === "Failed" || status === "Error" || status === "missing_content") { + throw new Error(Environment.getMistralDocumentProcessingFailedText(doc.fileName, status)); + } + await delay(2000, signal); + } + + if (!processed) { + throw new Error(Environment.getMistralDocumentProcessingTimedOutText(doc.fileName)); + } + + aiLog("success", "mistral.document.processed", { + libraryId, + documentId, + fileName: doc.fileName, + duration: aiLogDuration(documentStartedAt), + }); + + result.push({type: "document", documentId}); + } + + aiLog("success", "mistral.documents.prepare.done", { + libraryId, + count: docs.length, + duration: aiLogDuration(startedAt), + }); + return {documents: result, libraryId}; + } catch (e) { + aiLog("error", "mistral.documents.prepare.failed", { + libraryId, + duration: aiLogDuration(startedAt), + error: e, + }); + await deleteMistralLibrary(libraryId, target); + throw e; + } +} + +export async function executeTool( + toolCall: ToolCallData, + message: TelegramStreamMessage, + context: ToolRuntimeContext, +): Promise { + const startedAt = Date.now(); + + aiLog("info", "tool.start", { + tool: aiLogToolCall(toolCall), + hasPythonInputFiles: !!context.pythonInputFiles?.length, + }); + + await message.flush(); + + const parsedArgs = parseToolArgumentsObject(toolCall.argumentsText); + if (parsedArgs.ok === false) { + const result = toolFailureResult("invalid_arguments", parsedArgs.message, { + raw: parsedArgs.raw.slice(0, 4000), + }); + + aiLog("warn", "tool.arguments.invalid", { + tool: aiLogToolCall(toolCall), + duration: aiLogDuration(startedAt), + error: parsedArgs.message, + }); + + return result; + } + + try { + const rawResult = await executeToolCall(toolCall.name, parsedArgs.args, context); + const result = stringifyToolExecutionResult(rawResult); + + await sendToolArtifacts(toolCall, result, message); + + aiLog("success", "tool.done", { + name: toolCall.name, + duration: aiLogDuration(startedAt), + result, + }); + + return result; + } catch (error) { + if (isAbortError(error)) { + throw error; + } + + const result = toolFailureResult("execution_failed", errorMessage(error)); + + aiLog("error", "tool.failed.returned_to_model", { + name: toolCall.name, + duration: aiLogDuration(startedAt), + error, + }); + + return result; + } +} + +export function toolResourceKeys(toolCall: ToolCallData): string[] { + const args = safeJsonParseObject(toolCall.argumentsText); + const pathValue = typeof args.path === "string" ? args.path : undefined; + const sourcePath = typeof args.sourcePath === "string" ? args.sourcePath : undefined; + const targetPath = typeof args.targetPath === "string" ? args.targetPath : undefined; + + switch (toolCall.name) { + case "get_datetime": + case "web_search": + case "get_weather": + case "read_file": + case "list_directory": + return []; + case "create_file": + case "create_directory": + case "update_file": + case "delete_path": + return [`file:${pathValue ?? "*"}`]; + case "copy_path": + case "rename_path": + return [`file:${sourcePath ?? "*"}`, `file:${targetPath ?? "*"}`]; + case "shell_execute": + return ["shell:*"]; + default: + return [`tool:${toolCall.name}`]; + } +} + +export async function runWithToolLocks(keys: string[], task: () => Promise): Promise { + const uniqueKeys = [...new Set(keys)].sort(); + const run = (index: number): Promise => { + const key = uniqueKeys[index]; + if (!key) return task(); + return toolResourceLocks.runExclusive(key, () => run(index + 1)); + }; + + return run(0); +} + +export async function executeScheduledTool( + toolCall: ToolCallData, + message: TelegramStreamMessage, + context: ToolRuntimeContext, +): Promise { + const keys = toolResourceKeys(toolCall); + if (!keys.length) return executeTool(toolCall, message, context); + return runWithToolLocks(keys, () => executeTool(toolCall, message, context)); +} + +export async function executeToolBatch( + toolCalls: ToolCallData[], + message: TelegramStreamMessage, + context: ToolRuntimeContext, + memory: ToolExecutionMemory = new Map(), +): Promise { + if (!toolCalls.length) return []; + + const statusCalls = dedupeToolCalls(toolCalls); + + const startedAt = Date.now(); + + aiLog("info", "tool.batch.start", { + count: toolCalls.length, + uniqueCount: statusCalls.length, + tools: statusCalls.map(aiLogToolCall), + }); + + message.setStatus(Environment.getUseToolText(statusCalls)); + await message.flush(); + + const inBatch = new Map>(); + + const runOne = async (toolCall: ToolCallData): Promise => { + const signature = toolCallSignature(toolCall); + const previous = memory.get(signature); + + if (previous && previous.count >= MAX_IDENTICAL_TOOL_CALLS) { + const suppressed = duplicateToolCallSuppressedResult(toolCall, previous.result); + + aiLog("warn", "tool.duplicate.suppressed", { + tool: aiLogToolCall(toolCall), + previousCount: previous.count, + }); + + return suppressed; + } + + message.setStatus(Environment.getUseToolText(statusCalls)); + await message.flush(); + + const resultText = await executeScheduledTool(toolCall, message, context); + + memory.set(signature, { + count: (previous?.count ?? 0) + 1, + result: resultText, + }); + + message.setStatus(Environment.getUseToolText(statusCalls)); + await message.flush(); + + return resultText; + }; + + try { + const results = await Promise.all(toolCalls.map(toolCall => { + const signature = toolCallSignature(toolCall); + const existing = inBatch.get(signature); + + if (existing) { + aiLog("warn", "tool.duplicate.in_batch_joined", { + tool: aiLogToolCall(toolCall), + }); + + return existing; + } + + const promise = runOne(toolCall); + inBatch.set(signature, promise); + return promise; + })); + + message.setStatus(Environment.getUseToolText(statusCalls)); + await message.flush(); + + aiLog("success", "tool.batch.done", { + count: toolCalls.length, + uniqueCount: statusCalls.length, + duration: aiLogDuration(startedAt), + }); + + return results; + } catch (e) { + aiLog("error", "tool.batch.failed", { + count: toolCalls.length, + duration: aiLogDuration(startedAt), + error: e, + }); + + throw e; + } +} + +export function appendOllamaToolResults(messages: ChatMessage[], calls: ToolCallData[], results: string[]): void { + for (const [index, call] of calls.entries()) { + messages.push({ + role: "tool", + content: results[index] ?? "", + tool_name: call.name + }); + } +} + +export function stringifyToolExecutionResult(result: unknown): string { + if (typeof result === "string") return result; + const json = JSON.stringify(toJsonValue(result) ?? String(result)); + return json ?? String(result); +} + +export type ToolExecutionMemory = Map; + +export function stableJsonStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableJsonStringify).join(",")}]`; + } + + if (isRecord(value)) { + return `{${Object.keys(value) + .sort() + .map(key => `${JSON.stringify(key)}:${stableJsonStringify(value[key])}`) + .join(",")}}`; + } + + return JSON.stringify(value); +} + +export function toolCallSignature(toolCall: ToolCallData): string { + const parsed = parseToolArgumentsObject(toolCall.argumentsText); + const argsSignature = parsed.ok + ? stableJsonStringify(parsed.args) + : `invalid-json:${toolCall.argumentsText}`; + + return `${toolCall.name}\u0000${argsSignature}`; +} + +export type StreamingToolCallChunkLike = { + id?: string; + index?: number; + name?: string; + function?: { + name?: string; + arguments?: string | JsonObject; + }; + arguments?: string | JsonObject; +}; + +export class StreamingToolCallAccumulator { + private readonly byKey = new Map(); + + constructor( + private readonly prefix: string, + private readonly round: number, + ) { + } + + add(chunks: readonly StreamingToolCallChunkLike[]): ToolCallData[] { + for (const [fallbackIndex, chunk] of chunks.entries()) { + const key = typeof chunk.index === "number" + ? `index:${chunk.index}` + : chunk.id + ? `id:${chunk.id}` + : `fallback:${fallbackIndex}`; + + const existing = this.byKey.get(key) ?? { + id: chunk.id ?? `${this.prefix}_${this.round}_${fallbackIndex}`, + name: "", + argumentsText: "", + }; + + if (chunk.id) { + existing.id = chunk.id; + } + + const name = chunk.function?.name ?? chunk.name; + if (name) { + existing.name = name; + } + + const args = chunk.function?.arguments ?? chunk.arguments; + if (typeof args === "string") { + existing.argumentsText += args; + } else if (args !== undefined) { + existing.argumentsText = JSON.stringify(args); + } + + this.byKey.set(key, existing); + } + + return this.snapshot(); + } + + snapshot(): ToolCallData[] { + return [...this.byKey.values()].filter(call => call.name); + } +} + +export function duplicateToolCallSuppressedResult(toolCall: ToolCallData, previousResult: string): string { + return JSON.stringify({ + success: false, + skipped: true, + reason: `Identical tool call '${toolCall.name}' with the same arguments was already executed in this agentic loop. Use the previous result and answer the user instead of calling the tool again.`, + previousResult: previousResult.slice(0, 8000), + }); +} + +export function dedupeToolCalls(calls: ToolCallData[]): ToolCallData[] { + const bySignature = new Map(); + + for (const call of calls) { + bySignature.set(toolCallSignature(call), call); + } + + return [...bySignature.values()]; +} + +export type NormalizedRouterPlanStep = { + t: string[]; // Tool name + h: string; // Hint + from: string; +}; + +export type NormalizedRouterPlan = { + s: NormalizedRouterPlanStep[]; // Steps + m: string; // Missing +}; + +export function toolSchemaName(tool: unknown): string | undefined { + if (!isRecord(tool)) return undefined; + const fn = isRecord(tool.function) ? tool.function : undefined; + const directName = fn?.name ?? tool.name; + return asOptionalString(directName); +} + +export function toolSchemaNames(tool: unknown): string[] { + if (!isRecord(tool)) return []; + + if (Array.isArray(tool.functionDeclarations)) { + return tool.functionDeclarations + .map(declaration => isRecord(declaration) ? asOptionalString(declaration.name) : undefined) + .filter((name): name is string => !!name); + } + + const name = toolSchemaName(tool); + return name ? [name] : []; +} + +export function allToolSchemaNames(tools: readonly unknown[]): string[] { + return [...new Set(tools.flatMap(toolSchemaNames))]; +} + +export function getOpenAIResponsesToolsWithImage(config: RuntimeConfigSnapshot): Array { + return [ + ...getOpenAIResponsesTools(), + { + type: "image_generation", + model: config.openAiImageTarget.model, + size: "auto", + moderation: "low", + output_format: "png", + partial_images: OPENAI_IMAGE_PARTIALS, + }, + ]; +} + +export function collectOpenAiResponseText(response: OpenAiResponseLike): string { + if (typeof response.output_text === "string") return response.output_text; + + return (response.output ?? []) + .filter(item => item.type === "message") + .flatMap(item => item.content ?? []) + .map(content => content.text ?? content.refusal ?? "") + .join(""); +} + +export function collectOpenAiResponseFunctionCalls(response: OpenAiResponseLike): OpenAiResponsesFunctionCall[] { + return (response.output ?? []) + .filter(item => item.type === "function_call" && item.call_id && item.name) + .map(item => ({ + callId: item.call_id!, + name: item.name!, + argumentsText: item.arguments ?? "{}", + })); +} + +export function collectOpenAiResponseImages(response: OpenAiResponseLike): string[] { + return (response.output ?? []) + .filter(item => item.type === "image_generation_call" && typeof item.result === "string") + .map(item => item.result!); +} + +export function writeOpenAiGeneratedImage(sourceMessage: Message, b64: string, label: string): Buffer { + const imageBuffer = Buffer.from(b64, "base64"); + const fileName = `${sourceMessage.chat.id}_${sourceMessage.message_id}_${Date.now()}_${label}.png`; + fs.writeFileSync(path.join(photoGenDir, fileName), imageBuffer); + return imageBuffer; +} + +export async function showOpenAiGeneratedImage( + streamMessage: TelegramStreamMessage, + sourceMessage: Message, + b64: string, + label: string, + status: string, + final: boolean, +): Promise { + const imageBuffer = writeOpenAiGeneratedImage(sourceMessage, b64, label); + if (final && !streamMessage.getText().trim()) { + streamMessage.replaceText(status); + streamMessage.clearStatus(); + } else { + streamMessage.setStatus(status); + } + await streamMessage.showImage(imageBuffer); +} + +export function openAiResponseItemCallId(item: OpenAiResponseOutputItem & { id?: string }): string { + return item.call_id ?? item.id ?? `openai_response_${Date.now()}`; +} + diff --git a/src/ai/unified-ai-runner.tool-ranker.ts b/src/ai/unified-ai-runner.tool-ranker.ts new file mode 100644 index 0000000..218a770 --- /dev/null +++ b/src/ai/unified-ai-runner.tool-ranker.ts @@ -0,0 +1,225 @@ +import {Tool} from "ollama"; +import {AiRuntimeTarget, createOllamaClient} from "./ai-runtime-target"; +import {aiLog, aiLogDuration, aiLogProviderTarget} from "./ai-logger"; +import {allToolSchemaNames, isRecord, JsonObject, RuntimeConfigSnapshot, safeJsonParseObject, toolSchemaNames} from "./unified-ai-runner.shared"; + +type RankedToolStep = { + t: string | string[]; + h?: string; + from?: string; +}; + +type RankedToolPlan = { + s?: RankedToolStep[]; + m?: string; +}; + +export type ToolRankerSelection = { + tools: Tool[]; + selectedNames: string[]; + missing: string; + raw: string; + usedRanker: boolean; +}; + +export class OllamaToolRanker { + constructor(private readonly config: RuntimeConfigSnapshot) {} + + async selectTools(args: { + userQuery: string; + availableTools: Tool[]; + round: number; + signal: AbortSignal; + }): Promise { + const {availableTools, round, signal, userQuery} = args; + const target = this.config.ollamaToolRankerTarget; + + if (!availableTools.length) { + return {tools: [], selectedNames: [], missing: "", raw: "", usedRanker: false}; + } + + // Ranker disabled/unconfigured: keep old behavior and expose all allowed tools. + if (!target?.model) { + return { + tools: availableTools, + selectedNames: allToolSchemaNames(availableTools), + missing: "", + raw: "", + usedRanker: false, + }; + } + + const startedAt = Date.now(); + const availableNames = new Set(allToolSchemaNames(availableTools)); + const prompt = this.config.rankerToolPrompt?.trim() || DEFAULT_TOOL_RANKER_PROMPT; + const toolsForPrompt = availableTools.map(tool => ({ + names: toolSchemaNames(tool), + schema: tool, + })); + + aiLog("debug", "ollama.tool_ranker.start", { + round, + target: aiLogProviderTarget(target), + queryChars: userQuery.length, + availableTools: [...availableNames], + }); + + try { + const ollama = createOllamaClient(target as AiRuntimeTarget); + const response = await ollama.chat({ + model: target.model, + messages: [ + {role: "system", content: prompt}, + { + role: "user", + content: JSON.stringify({ + q: userQuery, + tools: toolsForPrompt, + }), + }, + ], + stream: false, + options: { + temperature: 0, + num_ctx: 8192, + }, + }); + + if (signal.aborted) throw new Error("Aborted"); + + const raw = response.message?.content?.trim() ?? ""; + const plan = parseToolRankerPlan(raw); + const selectedNames = normalizeToolRankerNames(plan, availableNames); + const selectedNameSet = new Set(selectedNames); + const tools = availableTools.filter(tool => toolSchemaNames(tool).some(name => selectedNameSet.has(name))); + const missing = typeof plan?.m === "string" ? plan.m.trim() : ""; + + aiLog("debug", "ollama.tool_ranker.done", { + round, + duration: aiLogDuration(startedAt), + selectedNames, + selectedCount: tools.length, + missing, + rawPreview: raw.slice(0, 800), + }); + + // Important: if the plan is valid and empty, use NO tools. Do not fall back to all tools. + return {tools, selectedNames, missing, raw, usedRanker: true}; + } catch (error) { + if (String(error).includes("Aborted")) throw error; + + aiLog("warn", "ollama.tool_ranker.failed.fallback_all_allowed", { + round, + target: aiLogProviderTarget(target), + duration: aiLogDuration(startedAt), + error, + }); + + // Ranker transport/model failure is different from "ranker returned empty plan". + // In that case, preserve availability rather than silently disabling tools. + return { + tools: availableTools, + selectedNames: allToolSchemaNames(availableTools), + missing: "", + raw: "", + usedRanker: false, + }; + } + } +} + +export function latestUserTextFromOllamaMessages(messages: readonly { role?: string; content?: unknown }[]): string { + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + if (message?.role !== "user") continue; + if (typeof message.content === "string") return message.content; + if (Array.isArray(message.content)) { + return message.content + .map(part => isRecord(part) && typeof part.text === "string" ? part.text : "") + .filter(Boolean) + .join("\n"); + } + } + return ""; +} + +export function looksLikeToolRankerJson(text: string): boolean { + const parsed = safeJsonParseObject(extractJsonObjectText(text) ?? text); + return Array.isArray(parsed.s) && typeof parsed.m === "string"; +} + +function parseToolRankerPlan(raw: string): RankedToolPlan | undefined { + const jsonText = extractJsonObjectText(raw); + if (!jsonText) return undefined; + + const parsed = safeJsonParseObject(jsonText) as JsonObject; + if (!Array.isArray(parsed.s)) return undefined; + + return parsed as RankedToolPlan; +} + +function normalizeToolRankerNames(plan: RankedToolPlan | undefined, availableNames: Set): string[] { + if (!plan?.s?.length) return []; + + const result: string[] = []; + for (const step of plan.s) { + const rawNames = Array.isArray(step.t) ? step.t : [step.t]; + for (const rawName of rawNames) { + if (typeof rawName !== "string") continue; + const name = rawName.trim(); + if (availableNames.has(name) && !result.includes(name)) { + result.push(name); + } + } + } + return result; +} + +function extractJsonObjectText(raw: string): string | undefined { + const text = raw.trim() + .replace(/^```(?:json)?\s*/i, "") + .replace(/\s*```$/i, "") + .trim(); + + const start = text.indexOf("{"); + if (start === -1) return undefined; + + let depth = 0; + let inString = false; + let escaped = false; + + for (let i = start; i < text.length; i++) { + const ch = text[i]; + + if (escaped) { + escaped = false; + continue; + } + + if (ch === "\\") { + escaped = true; + continue; + } + + if (ch === '"') { + inString = !inString; + continue; + } + + if (inString) continue; + + if (ch === "{") depth++; + if (ch === "}") depth--; + + if (depth === 0) { + return text.slice(start, i + 1); + } + } + + return undefined; +} + +const DEFAULT_TOOL_RANKER_PROMPT = `You are a tool router. Return strict compact JSON only. +Schema: {"s":[{"t":"tool_name","h":"short input hint","from":"previous_tool.output_or_empty"}],"m":""} +Use tools only when they are needed. If no tool is needed, return {"s":[],"m":""}. +Never answer the user. Never explain. Never use markdown.`; diff --git a/src/ai/unified-ai-runner.ts b/src/ai/unified-ai-runner.ts index 8b83d02..37b247f 100644 --- a/src/ai/unified-ai-runner.ts +++ b/src/ai/unified-ai-runner.ts @@ -1,2078 +1,31 @@ -import {Message} from "typescript-telegram-bot-api"; -import fs, {createReadStream, openAsBlob} from "node:fs"; -import path from "node:path"; +// Facade extracted from unified-ai-runner.ts. import {AiProvider} from "../model/ai-provider"; import {Environment} from "../common/environment"; -import {bot, notesDir, photoGenDir} from "../index"; -import {clamp, collectReplyChainText, delay, ifTrue, logError, replyToMessage} from "../util/utils"; -import {MessageStore} from "../common/message-store"; -import { - AiProviderName, - getGeminiTools, - getMistralTools, - getOllamaTools, - getOpenAIResponsesTools, - getOpenAITools -} from "./tool-mappers"; +import {ifTrue, logError, replyToMessage} from "../util/utils"; import {createAiCancelRequest, finishAiRequest, setAiCancelMessageId} from "./cancel-registry"; -import {TelegramArtifactFile, TelegramStreamMessage} from "./telegram-stream-message"; +import {TelegramStreamMessage} from "./telegram-stream-message"; import {AiDownloadedFile, attachmentsToDownloadedFiles, cleanupDownloads} from "./telegram-attachments"; -import {getModelCapabilities, getRuntimeCapabilities} from "./provider-model-runtime"; -import {StoredAttachment} from "../model/stored-attachment"; -import {AiChatMessage, ChatMessage} from "./chat-messages-types"; -import {ChatRequest, ListResponse, Ollama, Tool} from "ollama"; -import {executeToolCall, ToolRuntimeContext} from "./tools/runtime"; -import {MessageImagePart, MessagePart} from "../common/message-part"; -import {enqueueTelegramApiCall} from "../util/telegram-api-queue"; -import {KeyedAsyncLock} from "../util/async-lock"; -import {getCurrentDateTimeTool} from "./tools/datetime"; -import {getMarketRatesTool} from "./tools/market-rates"; -import {getWeatherTool} from "./tools/weather"; -import {aiProviderRequestQueue, type AiRequestQueueTarget} from "./provider-request-queue"; -import {loadOllamaModel, RouterPlanSchema, unloadAllOllamaModels} from "./tools/utils"; +import {ChatMessage} from "./chat-messages-types"; +import {aiProviderRequestQueue} from "./provider-request-queue"; import {prepareOllamaDocumentRag} from "./ollama-rag"; -import {PYTHON_INTERPRETER_TOOL_NAME, pythonInterpreterToolPrompt} from "./tools/python-interpretator"; -import { - AI_VOICE_MODE_TRANSCRIPT, - DEFAULT_AI_RESPONSE_LANGUAGE, - getResponseLanguageInstruction, - resolveAiContextSizeForUser, - resolveAiResponseLanguageForUser, - resolveAiVoiceModeForUser, - UserAiResponseLanguage, - UserAiVoiceMode, -} from "../common/user-ai-settings"; -import { - isTranscribableAudioDownload, - resolveSpeechToTextProviderForUser, - transcribeSpeechDownloads, -} from "./speech-to-text"; +import {AI_VOICE_MODE_TRANSCRIPT, DEFAULT_AI_RESPONSE_LANGUAGE, resolveAiContextSizeForUser, resolveAiResponseLanguageForUser, resolveAiVoiceModeForUser} from "../common/user-ai-settings"; +import {isTranscribableAudioDownload} from "./speech-to-text"; import {OpenAIChatMessage} from "./openai-chat-message"; -import {ResponseInputMessageContentList} from "openai/resources/responses/responses.js"; import {MistralChatMessage} from "./mistral-chat-message"; import {OllamaChatMessage} from "./ollama-chat-message"; import {GeminiMessage} from "./gemini-chat-message"; import {buildAiRegenerateCallbackData} from "./regenerate-callback"; -import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer"; -import { - AiRuntimeTarget, - createGeminiOpenAiClient, - createGoogleGenAiClient, - createMistralClient, - createOllamaClient, - createOpenAiClient, - getGeminiApiMode, - resolveAiRuntimeTarget, -} from "./ai-runtime-target"; -import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/send-note-file"; +import {createOllamaClient, getGeminiApiMode} from "./ai-runtime-target"; +import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget} from "./ai-logger"; -const TELEGRAM_LIMIT = 4096; -const MAX_TOOL_ROUNDS = 20; -const OPENAI_IMAGE_PARTIALS = 3; -const AI_REQUEST_TIMEOUT_MS = 10 * 60 * 1000; -const MIN_OLLAMA_CONTEXT_SIZE = 4096; -const MAX_OLLAMA_CONTEXT_SIZE = 262144; -const DEFAULT_OLLAMA_CONTEXT_SIZE = 32768; -const toolResourceLocks = new KeyedAsyncLock(); +import {runOpenAi, runOpenAiCompatibleChat} from "./unified-ai-runner.openai"; +import {runOllama} from "./unified-ai-runner.ollama"; +import {runMistral} from "./unified-ai-runner.mistral"; +import {runGemini} from "./unified-ai-runner.gemini"; +import {AI_REQUEST_TIMEOUT_MS, TELEGRAM_LIMIT, RuntimeConfigSnapshot, UnifiedRunOptions, appendTranscriptToChatMessages, collectCachedMessageAttachments, collectRequestedAttachmentKinds, collectTextMessages, deleteMistralLibrary, hasAudioAttachmentKind, initialStatus, isAbortError, prepareMistralDocuments, providerName, rejectUnsupportedAttachments, resolveAiRequestQueueTarget, snapshotModel, snapshotRuntimeConfig, stripAudioFromRunnerMessages, toolRuntimeContextFromDownloads, transcribeAudioIfNeeded} from "./unified-ai-runner.shared"; -type UnifiedRunOptions = { - provider: AiProvider; - msg: Message; - isGuestMsg?: boolean; - text: string; - stream?: boolean; - think?: Think; - responseLanguage?: UserAiResponseLanguage; - contextSize?: number; - voiceMode?: UserAiVoiceMode; - targetMessage?: Message; -}; - -export type ToolCallData = { - id: string; - name: string; - argumentsText: string; -}; - -type OpenAiResponsesFunctionCall = { - callId: string; - name: string; - argumentsText: string; -}; - -// type OpenAiRunnerMessage = { -// role: "system" | "user" | "assistant"; -// content: string | Array>; -// }; - -// type MistralRunnerMessage = { -// role: "system" | "user" | "assistant" | "tool"; -// content?: string | Array>; -// name?: string; -// toolCallId?: string; -// toolCalls?: Array<{ -// id: string; -// function: { -// name: string; -// arguments: string; -// }; -// }>; -// }; - -// type GeminiRunnerInput = Array> -// type RunnerMessage = ChatMessage | OpenAiRunnerMessage | MistralRunnerMessage | GeminiRunnerInput; - -type AttachmentKind = "image" | "document" | "audio" | "video" | "video-note"; - -type Think = boolean | "high" | "medium" | "low"; - -type RuntimeConfigSnapshot = { - useNamesInPrompt: boolean; - useSystemPrompt: boolean; - systemPrompt?: string; - rankerToolPrompt?: string; - - ollamaChatTarget: AiRuntimeTarget; - ollamaToolRankerTarget?: AiRuntimeTarget; - ollamaVisionTarget: AiRuntimeTarget; - ollamaThinkingTarget: AiRuntimeTarget; - ollamaAudioTarget: AiRuntimeTarget; - ollamaDocumentsTarget: AiRuntimeTarget; - ollamaRagChunkSize: number; - ollamaRagChunkOverlap: number; - ollamaRagTopK: number; - ollamaRagMaxContextChars: number; - ollamaRagMinScore: number; - ollamaRagMaxArchiveFiles: number; - ollamaRagMaxArchiveBytes: number; - ollamaRagMaxArchiveDepth: number; - - geminiChatTarget: AiRuntimeTarget; - - mistralChatTarget: AiRuntimeTarget; - - openAiChatTarget: AiRuntimeTarget; - openAiImageTarget: AiRuntimeTarget; -}; - -// TODO: 11/05/2026, Danil Nikolaev: remove export -export function snapshotRuntimeConfig(): RuntimeConfigSnapshot { - return { - useNamesInPrompt: Environment.USE_NAMES_IN_PROMPT, - useSystemPrompt: Environment.USE_SYSTEM_PROMPT, - - systemPrompt: Environment.SYSTEM_PROMPT, - rankerToolPrompt: Environment.RANKER_TOOL_PROMPT, - - ollamaChatTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat"), - ollamaToolRankerTarget: { - provider: AiProvider.OLLAMA, - purpose: "tools", - model: "gemma4:e2b", - baseUrl: "http://meloda-zen.lan:11434" - }, - ollamaVisionTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "vision"), - ollamaThinkingTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "thinking"), - ollamaAudioTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "audio"), - ollamaDocumentsTarget: resolveAiRuntimeTarget(AiProvider.OLLAMA, "documents"), - ollamaRagChunkSize: Environment.OLLAMA_RAG_CHUNK_SIZE, - ollamaRagChunkOverlap: Environment.OLLAMA_RAG_CHUNK_OVERLAP, - ollamaRagTopK: Environment.OLLAMA_RAG_TOP_K, - ollamaRagMaxContextChars: Environment.OLLAMA_RAG_MAX_CONTEXT_CHARS, - ollamaRagMinScore: Environment.OLLAMA_RAG_MIN_SCORE, - ollamaRagMaxArchiveFiles: Environment.OLLAMA_RAG_MAX_ARCHIVE_FILES, - ollamaRagMaxArchiveBytes: Environment.OLLAMA_RAG_MAX_ARCHIVE_BYTES, - ollamaRagMaxArchiveDepth: Environment.OLLAMA_RAG_MAX_ARCHIVE_DEPTH, - - geminiChatTarget: resolveAiRuntimeTarget(AiProvider.GEMINI, "chat"), - - mistralChatTarget: resolveAiRuntimeTarget(AiProvider.MISTRAL, "chat"), - - openAiChatTarget: resolveAiRuntimeTarget(AiProvider.OPENAI, "chat"), - openAiImageTarget: resolveAiRuntimeTarget(AiProvider.OPENAI, "outputImages"), - }; -} - -function getMessageImageParts(part: MessagePart): MessageImagePart[] { - if (part.imageParts?.length) return part.imageParts; - return (part.images ?? []).map(data => ({data, mimeType: "image/jpeg"})); -} - -function openAiImageDataUrl(image: MessageImagePart): string { - return `data:${image.mimeType || "image/jpeg"};base64,${image.data}`; -} - -function geminiAudioMimeType(mimeType: string | undefined): string { - const normalized = mimeType?.toLowerCase(); - switch (normalized) { - case "audio/wav": - case "audio/mp3": - case "audio/aiff": - case "audio/aac": - case "audio/ogg": - case "audio/flac": - case "audio/mpeg": - case "audio/m4a": - case "audio/l16": - case "audio/opus": - case "audio/alaw": - case "audio/mulaw": - return normalized; - default: - return "audio/wav"; - } -} - -export function snapshotModel(provider: AiProvider, config: RuntimeConfigSnapshot): string { - switch (provider) { - case AiProvider.OLLAMA: - return config.ollamaChatTarget.model; - case AiProvider.GEMINI: - return config.geminiChatTarget.model; - case AiProvider.MISTRAL: - return config.mistralChatTarget.model; - case AiProvider.OPENAI: - return config.openAiChatTarget.model; - } -} - -export function providerTargets(provider: AiProvider, config: RuntimeConfigSnapshot): AiRuntimeTarget[] { - switch (provider) { - case AiProvider.OLLAMA: - return [ - config.ollamaChatTarget, - config.ollamaVisionTarget, - config.ollamaThinkingTarget, - config.ollamaAudioTarget, - config.ollamaDocumentsTarget - ]; - case AiProvider.GEMINI: - return [config.geminiChatTarget]; - case AiProvider.MISTRAL: - return [config.mistralChatTarget]; - case AiProvider.OPENAI: - return [config.openAiChatTarget]; - } -} - -function providerName(provider: AiProvider): AiProviderName { - switch (provider) { - case AiProvider.OLLAMA: - return "ollama"; - case AiProvider.GEMINI: - return "gemini"; - case AiProvider.MISTRAL: - return "mistral"; - case AiProvider.OPENAI: - return "openai"; - } -} - -function buildSystemInstruction( - config: RuntimeConfigSnapshot, - responseLanguage: UserAiResponseLanguage, - includePythonToolPrompt: boolean, -): string { - return [ - getResponseLanguageInstruction(responseLanguage), - config.systemPrompt && config.useSystemPrompt ? config.systemPrompt : null, - includePythonToolPrompt ? pythonInterpreterToolPrompt : null, - ].filter(Boolean).join("\n\n"); -} - -function initialStatus(downloads: AiDownloadedFile[], messagePartsImages: number): string { - const documents = downloads.filter(d => d.kind === "document"); - const images = downloads.filter(d => d.kind === "image").length + messagePartsImages; - const audio = downloads.filter(isTranscribableAudioDownload).length; - - if (documents.length) return prepareTelegramMarkdownV2(Environment.getAnalyzingDocumentText(documents.map(d => d.fileName))); - if (audio) return Environment.transcribingAudioText; - if (images > 1) return Environment.analyzingPicturesText; - if (images === 1) return Environment.analyzingPictureText; - return Environment.waitThinkText; -} - -function hasAudioAttachmentKind(kinds: Set): boolean { - return kinds.has("audio") || kinds.has("video-note"); -} - -function resolveAiRequestQueueTarget( - options: Pick, - config: RuntimeConfigSnapshot, - requestedAttachmentKinds: Set, -): AiRequestQueueTarget { - switch (options.provider) { - case AiProvider.OLLAMA: - if (hasAudioAttachmentKind(requestedAttachmentKinds)) return config.ollamaAudioTarget; - if (requestedAttachmentKinds.has("image")) return config.ollamaVisionTarget; - return options.think ? config.ollamaThinkingTarget : config.ollamaChatTarget; - case AiProvider.GEMINI: - return config.geminiChatTarget; - case AiProvider.MISTRAL: - return config.mistralChatTarget; - case AiProvider.OPENAI: - return config.openAiChatTarget; - } -} - -function roundStatus(round: number, firstRoundStatus: string, content?: string, toolCalls?: ToolCallData[], thinking?: boolean): string | null { - if (content?.length && !toolCalls?.length && !thinking) { - // console.log("ROUND_STATUS", "null"); - return null; - } - - const status = toolCalls?.length ? Environment.getUseToolText(toolCalls) - : thinking ? Environment.reasoningText - : round === 0 ? firstRoundStatus - : Environment.waitThinkText; - - // console.log("ROUND_STATUS", status); - - return status; -} - -function isPlainTextDocument(doc: AiDownloadedFile): boolean { - const ext = path.extname(doc.fileName).toLowerCase(); - const mime = (doc.mimeType ?? "").toLowerCase(); - - return mime.startsWith("text/") - || mime === "application/json" - || mime === "application/xml" - || [ - ".txt", - ".md", - ".markdown", - ".csv", - ".json", - ".jsonl", - ".xml", - ".yaml", - ".yml", - ".ini", - ".env", - ".log", - ".ps1", - ".sh", - ".bat", - ".cmd", - ".js", - ".jsx", - ".ts", - ".tsx", - ".py", - ".rb", - ".go", - ".java", - ".c", - ".cc", - ".cpp", - ".h", - ".hpp", - ".php", - ".sql", - ].includes(ext); -} - -function decodeTextDocument(doc: AiDownloadedFile): string { - return doc.buffer.toString("utf8").replace(/\u0000/g, ""); -} - -export function ollamaModelNames(response: ListResponse): string[] { - return (response?.models ?? []) - .flatMap((model) => [model?.model, model?.name]) - .filter((value): value is string => typeof value === "string" && value.length > 0); -} - -async function isOllamaModelActive(ollama: Ollama, target: AiRuntimeTarget): Promise { - const active = await ollama.ps(); - return ollamaModelNames(active).includes(target.model); -} - -function addMessageAttachmentKinds(msg: Message | undefined, kinds: Set): void { - if (!msg) return; - - if (msg.photo?.length) kinds.add("image"); - if (msg.document) { - const mimeType = msg.document.mime_type; - kinds.add(mimeType?.startsWith("image/") ? "image" : mimeType?.startsWith("audio/") ? "audio" : "document"); - } - if (msg.voice || msg.audio || msg.video_note) kinds.add("audio"); -} - -async function collectStoredReplyChainAttachments(msg: Message): Promise { - const attachments: StoredAttachment[] = []; - const seen = new Set(); - let current = await MessageStore.get(msg.chat.id, msg.message_id); - - for (let i = 0; current && i < 40; i++) { - for (const attachment of current.attachments ?? []) { - const key = [ - attachment.kind, - attachment.fileUniqueId || attachment.fileId, - attachment.cachePath, - ].join(":"); - if (seen.has(key)) continue; - seen.add(key); - attachments.push(attachment); - } - current = await MessageStore.get(current.chatId, current.replyToMessageId); - } - - return attachments; -} - -async function hasStoredReplyChainImage(msg: Message): Promise { - const attachments = await collectStoredReplyChainAttachments(msg); - if (attachments.some(attachment => attachment.kind === "image")) return true; - - let current = await MessageStore.get(msg.chat.id, msg.message_id); - - for (let i = 0; current && i < 40; i++) { - if (current.photoMaxSizeFilePath?.length) return true; - current = await MessageStore.get(current.chatId, current.replyToMessageId); - } - - return false; -} - -async function collectRequestedAttachmentKinds(msg: Message): Promise> { - const kinds = new Set(); - - addMessageAttachmentKinds(msg, kinds); - addMessageAttachmentKinds(msg.reply_to_message, kinds); - - for (const attachment of await collectStoredReplyChainAttachments(msg)) { - kinds.add(attachment.kind); - } - - if (!kinds.has("image") && await hasStoredReplyChainImage(msg)) { - kinds.add("image"); - } - - return kinds; -} - -function unsupportedAttachmentText(provider: AiProvider, model: string, kind: AttachmentKind): string { - const providerName = provider.toLowerCase(); - - switch (kind) { - case "audio": - return Environment.getCurrentModelUnsupportedInputText(model, providerName, "voice or audio messages"); - case "image": - return Environment.getCurrentModelUnsupportedInputText(model, providerName, "images"); - case "document": - return Environment.getCurrentModelUnsupportedInputText(model, providerName, "documents"); - case "video": - return Environment.getCurrentModelUnsupportedInputText(model, providerName, "video"); - case "video-note": - return Environment.getCurrentModelUnsupportedInputText(model, providerName, "video notes"); - } -} - -async function rejectUnsupportedAttachments( - provider: AiProvider, - model: string, - msg: Message, - config: RuntimeConfigSnapshot, - requestedAttachmentKinds?: Set, -): Promise { - const kinds = requestedAttachmentKinds ?? await collectRequestedAttachmentKinds(msg); - let effectiveModel = model || snapshotModel(provider, config); - const hasAudio = hasAudioAttachmentKind(kinds); - - if (provider === AiProvider.OLLAMA) { - effectiveModel = hasAudio ? config.ollamaAudioTarget.model - : kinds.has("image") ? config.ollamaVisionTarget.model - : config.ollamaChatTarget.model; - } - - const caps = await getRuntimeCapabilities(provider, effectiveModel); - - let speechToTextSupported = !hasAudio; - if (hasAudio && msg.from?.id) { - speechToTextSupported = await resolveSpeechToTextProviderForUser(msg.from.id, provider) - .then(() => true) - .catch(() => false); - } - - const unsupported = - (hasAudio && !speechToTextSupported ? "audio" : null) ?? - (kinds.has("image") && !caps.vision?.supported ? "image" : null) ?? - (kinds.has("document") && !caps.documents?.supported ? "document" : null); - - if (!unsupported) return false; - - if (!kinds.has("audio")) { - await replyToMessage({ - message: msg, - text: unsupportedAttachmentText(provider, effectiveModel, unsupported), - }).catch(logError); - } - - return true; -} - -async function collectCachedMessageAttachments(msg: Message): Promise<{ - attachments: StoredAttachment[]; - missing: StoredAttachment[] -}> { - const attachments = await collectStoredReplyChainAttachments(msg); - return { - attachments, - missing: attachments.filter(attachment => !fs.existsSync(attachment.cachePath)), - }; -} - -function safeJsonParseObject(value?: string): Record { - if (!value?.trim()) return {}; - try { - const parsed = JSON.parse(value); - return typeof parsed === "object" && parsed && !Array.isArray(parsed) ? parsed : {}; - } catch { - return {}; - } -} - -function toolRuntimeContextFromDownloads(downloads: AiDownloadedFile[]): ToolRuntimeContext { - if (!downloads.length) return {}; - - return { - pythonInputFiles: downloads.map(download => ({ - kind: download.kind, - path: download.path, - fileName: download.fileName, - mimeType: download.mimeType, - })), - }; -} - -function extractToolArtifacts(toolName: string, result: string): TelegramArtifactFile[] { - if (toolName !== PYTHON_INTERPRETER_TOOL_NAME) return []; - - try { - const parsed = JSON.parse(result) as Record; - const artifacts = Array.isArray(parsed.artifacts) ? parsed.artifacts : []; - - return artifacts - .map(artifact => artifact as Partial) - .filter((artifact): artifact is TelegramArtifactFile => { - return (artifact.kind === "image" || artifact.kind === "file") - && typeof artifact.path === "string" - && typeof artifact.fileName === "string" - && Number.isSafeInteger(artifact.sizeBytes); - }); - } catch { - return []; - } -} - -async function sendToolArtifacts(toolCall: ToolCallData, result: string, message: TelegramStreamMessage): Promise { - const artifacts = extractToolArtifacts(toolCall.name, result); - for (const artifact of artifacts) { - await message.sendArtifact(artifact); - } -} - -function normalizeMistralToolCalls(calls: any[] = []): ToolCallData[] { - return calls.map((call, i) => ({ - id: call.id || `call_${Date.now()}_${i}`, - name: call.function?.name || call.name || "", - argumentsText: typeof call.function?.arguments === "string" ? call.function.arguments : JSON.stringify(call.function?.arguments ?? call.arguments ?? {}), - })).filter(c => c.name); -} - -function contentFromMistralDelta(delta: any): string { - if (!delta?.content) return ""; - if (typeof delta.content === "string") return delta.content; - if (Array.isArray(delta.content)) return delta.content.map((c: any) => c.text ?? "").join(""); - return ""; -} - -function buildOpenAiResponseMessage(part: MessagePart, getContent: (part: MessagePart) => string): OpenAIChatMessage { - const content: ResponseInputMessageContentList = [{ - type: "input_text", - text: getContent(part), - }]; - - if (!part.bot) { - for (const image of getMessageImageParts(part)) { - content.push({type: "input_image", image_url: openAiImageDataUrl(image), detail: "auto"}); - } - } - - return {role: part.bot ? "assistant" : "user", content, type: "message"}; -} - -function buildGeminiMessage(part: MessagePart, getContent: (part: MessagePart) => string): GeminiMessage { - const parts: GeminiMessage["parts"] = [{text: getContent(part)}]; - - if (!part.bot) { - for (const image of getMessageImageParts(part)) { - parts.push({ - inlineData: { - data: image.data, - mimeType: image.mimeType || "image/jpeg", - }, - }); - } - - const audioParts = part.audioParts?.length - ? part.audioParts - : (part.audios ?? []).map(data => ({data, mimeType: "audio/wav"})); - - for (const audio of audioParts) { - parts.push({ - inlineData: { - data: audio.data, - mimeType: geminiAudioMimeType(audio.mimeType), - }, - }); - } - - for (const videoNote of part.videoNotes ?? []) { - parts.push({ - inlineData: { - data: videoNote, - mimeType: "audio/wav", - }, - }); - } - } - - return { - role: part.bot ? "model" : "user", - parts, - }; -} - -async function collectTextMessages( - msg: Message, - textOverride: string, - provider: AiProvider, - downloads: AiDownloadedFile[], - config: RuntimeConfigSnapshot, - responseLanguage: UserAiResponseLanguage, -): Promise<{ - chatMessages: AiChatMessage[]; - imageCount: number -}> { - const storedMsg = await MessageStore.get(msg.chat.id, msg.message_id); - const messageParts = await collectReplyChainText({triggerMsg: storedMsg, downloads: downloads}); - - if (messageParts.length && textOverride?.trim()) { - // const latest = messageParts[0]; - // if (!latest.bot && textOverride?.trim()) latest.content = textOverride.trim(); - } - - const ordered = messageParts.reverse(); - const imageCount = ordered.reduce((sum, p) => sum + (p.imageParts?.length ?? p.images?.length ?? 0), 0); - const includePythonToolPrompt = Environment.ENABLE_PYTHON_INTERPRETER && msg.from?.id === Environment.CREATOR_ID; - const systemInstruction = buildSystemInstruction(config, responseLanguage, includePythonToolPrompt); - - const getContent = (part: MessagePart): string => { - if (part.bot) return part.content; - - const userInfo = [ - "[user_info]:", - `name: ${part.name}`, - `username: @${part.userName}`, - "" - ].join("\n"); - - const finalContent = [ - part.content - ]; - - if (Environment.USE_NAMES_IN_PROMPT) { - finalContent.unshift(userInfo); - } - - return finalContent.join("\n"); - }; - - if (provider === AiProvider.OPENAI) { - const messages: OpenAIChatMessage[] = ordered.map(part => buildOpenAiResponseMessage(part, getContent)); - - if (systemInstruction) { - messages.unshift({role: "system", content: systemInstruction, type: "message"}); - } - return {chatMessages: messages, imageCount}; - } - - if (provider === AiProvider.MISTRAL) { - const messages: MistralChatMessage[] = ordered.map(part => { - if (part.bot) { - return { - role: "assistant", - content: [{type: "text", text: getContent(part)}] - }; - } else { - return { - role: "user", - content: [ - {type: "text", text: getContent(part)}, - ...getMessageImageParts(part).map(p => { - return { - type: "image_url" as const, - imageUrl: `data:${p.mimeType || "image/jpeg"};base64,${p.data}` - }; - }) - ] - }; - } - }); - - if (systemInstruction) { - messages.unshift({role: "system", content: systemInstruction}); - } - return {chatMessages: messages, imageCount}; - } - - if (provider === AiProvider.OLLAMA) { - const messages: OllamaChatMessage[] = ordered.map(part => ({ - role: part.bot ? "assistant" : "user", - content: getContent(part), - images: part.bot ? undefined : part.images, - imageParts: part.imageParts, - audios: part.audios, - audioParts: part.audioParts, - videos: part.videos, - videoNotes: part.videoNotes - })); - - if (systemInstruction) { - /* - "[current_date]:\n" + new Date().toLocaleTimeString([], { - day: "2-digit", - month: "2-digit", - year: "numeric", - }) + "\n\n" + - */ - - messages.unshift({ - role: "system", - content: systemInstruction - }); - } - - return {chatMessages: messages, imageCount}; - } - - if (provider === AiProvider.GEMINI) { - if (getGeminiApiMode(config.geminiChatTarget) === "openai") { - const messages: OpenAIChatMessage[] = ordered.map(part => buildOpenAiResponseMessage(part, getContent)); - if (systemInstruction) { - messages.unshift({role: "system", content: systemInstruction, type: "message"}); - } - - return {chatMessages: messages, imageCount}; - } - - const messages: GeminiMessage[] = ordered.map(part => buildGeminiMessage(part, getContent)); - if (systemInstruction) { - messages.unshift({ - role: "user", - parts: [{text: systemInstruction}], - }); - } - - return {chatMessages: messages, imageCount}; - } - - // const messages: RunnerMessage[] = ordered.map(part => { - // return { - // role: part.bot ? "assistant" : "user", - // content: `${config.useNamesInPrompt && !part.bot ? `${part.name}:\n` : ""}${part.content}`, - // images: part.images, - // imageParts: part.imageParts, - // audios: part.audios, - // audioParts: part.audioParts, - // documents: part.documents, - // }; - // }); - // - // if (systemInstruction) { - // messages.unshift({role: "system", content: systemInstruction}); - // } - // - return {chatMessages: [], imageCount: -1}; -} - -// function collectGeminiOutputText(response: any): string { -// if (typeof response?.output_text === "string") return response.output_text; -// if (typeof response?.text === "string") return response.text; -// -// return (response?.outputs ?? []) -// .map((output: any) => { -// if (typeof output === "string") return output; -// if (output?.type === "text") return output.text ?? ""; -// if (typeof output?.text === "string") return output.text; -// return ""; -// }) -// .join(""); -// } - -// function collectGeminiOutputImages(response: any): string[] { -// return (response?.outputs ?? []) -// .filter((output: any) => output?.type === "image" && (output.data || output.image_base64)) -// .map((output: any) => output.data ?? output.image_base64); -// } - -// async function maybeGenerateGeminiImage(msg: Message, text: string, streamMessage: TelegramStreamMessage, signal: AbortSignal, config: RuntimeConfigSnapshot): Promise { -// if (signal.aborted) throw new Error("Aborted"); -// -// streamMessage.setStatus(Environment.genImageText); -// await streamMessage.flush(); -// -// const response = await geminiAi.interactions.create({ -// model: config.geminiImageModel, -// input: text, -// response_modalities: ["image"], -// }); -// -// if (signal.aborted) throw new Error("Aborted"); -// -// const images = collectGeminiOutputImages(response); -// const imageB64 = images[images.length - 1]; -// -// if (!imageB64) throw new Error("Gemini did not return image.output/image_base64."); -// const imageBuffer = Buffer.from(imageB64, "base64"); -// -// await enqueueTelegramApiCall( -// () => bot.sendPhoto({ -// chat_id: msg.chat.id, -// photo: imageBuffer, -// caption: `👨‍🎨 Done. Model: ${config.geminiImageModel}`, -// reply_parameters: {message_id: msg.message_id}, -// }), -// {method: "sendPhoto", chatId: msg.chat.id, chatType: msg.chat.type} -// ); -// -// streamMessage.clearStatus(); -// streamMessage.replaceText("👨‍🎨 Image generated."); -// await streamMessage.finish(); - -// return true; -// } - -async function transcribeAudioIfNeeded(provider: AiProvider, userId: number | undefined, downloads: AiDownloadedFile[], message: TelegramStreamMessage, signal: AbortSignal): Promise { - if (!downloads.some(isTranscribableAudioDownload)) return ""; - if (!userId) throw new Error(Environment.couldNotIdentifyUserForSpeechToTextText); - if (signal.aborted) throw new Error("Aborted"); - - message.setStatus(Environment.transcribingAudioText); - await message.flush(); - - const resolved = await resolveSpeechToTextProviderForUser(userId, provider); - const transcript = await transcribeSpeechDownloads(resolved.provider, downloads, signal); - if (!transcript.trim()) { - throw new Error(Environment.speechToTextEmptyResultText); - } - return transcript; -} - -function stripAudioFromRunnerMessages(parts: AiChatMessage[]): void { - for (const part of parts) { - if ("audios" in part) { - delete part.audios; - } - if ("audioParts" in part) { - delete part.audioParts; - } - - if ("videoNotes" in part) { - delete part.videoNotes; - } - - if ("parts" in part && Array.isArray(part.parts)) { - part.parts = part.parts.filter(geminiPart => { - if (!("inlineData" in geminiPart)) return true; - const mimeType = geminiPart.inlineData.mimeType.toLowerCase(); - return !mimeType.startsWith("audio/") && !mimeType.startsWith("video/"); - }); - } - } -} - -function appendTranscriptToChatMessages(chatMessages: AiChatMessage[], provider: AiProvider, transcript: string): void { - const lastUser = [...chatMessages].reverse().find((p) => "role" in p && p.role === "user"); - if (!lastUser) return; - - const text = transcript; - - if (provider === AiProvider.GEMINI && "parts" in lastUser && Array.isArray(lastUser.parts)) { - lastUser.parts.push({text}); - return; - } - - if (!("content" in lastUser)) return; - - if (typeof lastUser.content === "string") { - lastUser.content = [lastUser.content, text].filter((value: string) => value.trim()).join("\n\n"); - return; - } - - if (Array.isArray(lastUser.content)) { - lastUser.content.push({ - type: provider === AiProvider.OPENAI ? "input_text" : "text", - text: text, - } as any); - } -} - -// async function sendVoiceResponseIfNeeded(options: UnifiedRunOptions, downloads: AiDownloadedFile[], text: string): Promise { -// if (!downloads.some(isTranscribableAudioDownload)) return; -// if (!options.msg.from?.id) return; -// -// const trimmed = text.trim(); -// if (!trimmed) return; -// -// try { -// const provider = (await resolveTextToSpeechProviderForUser(options.msg.from.id)).provider; -// const speech = await synthesizeSpeech({provider, text: trimmed}); -// await sendSynthesizedSpeech(options.msg, speech); -// } catch (e) { -// logError(e); -// } -// } - -async function deleteMistralLibrary(libraryId: string | undefined, target: AiRuntimeTarget): Promise { - if (!libraryId) return; - - try { - const mistralAi = createMistralClient(target); - await mistralAi.beta.libraries.delete({libraryId}); - } catch (e) { - logError(e); - } -} - -async function appendMistralTextDocument(doc: AiDownloadedFile, messages: any[], message: TelegramStreamMessage): Promise { - message.setStatus(prepareTelegramMarkdownV2(Environment.getAnalyzingDocumentText([doc.fileName]))); - await message.flush(); - - const text = decodeTextDocument(doc).trim(); - if (!text) { - throw new Error(Environment.getDocumentIsEmptyText(doc.fileName)); - } - - messages.push({ - role: "user", - content: [ - { - type: "text", - text: Environment.getDocumentContentText(doc.fileName, text), - }, - ], - }); -} - -async function prepareMistralDocuments(downloads: AiDownloadedFile[], messages: any[], message: TelegramStreamMessage, target: AiRuntimeTarget, signal: AbortSignal): Promise<{ - documents: any[]; - libraryId?: string -}> { - const docs = downloads.filter(d => d.kind === "document"); - const result: any[] = []; - if (!docs.length) return {documents: result}; - - const mistralAi = createMistralClient(target); - const library: any = await mistralAi.beta.libraries.create({ - name: `tg-chat-bot-${Date.now()}`, - description: "Temporary library for document search", - }, {signal}); - const libraryId = library?.id; - if (!libraryId) { - throw new Error(Environment.mistralLibraryIdMissingText); - } - - try { - for (const doc of docs) { - if (signal.aborted) throw new Error("Aborted"); - - if (isPlainTextDocument(doc)) { - await appendMistralTextDocument(doc, messages, message); - continue; - } - - message.setStatus(prepareTelegramMarkdownV2(Environment.getAnalyzingDocumentText([doc.fileName]))); - await message.flush(); - - const uploaded: any = await mistralAi.beta.libraries.documents.upload({ - libraryId, - requestBody: { - file: await openAsBlob(doc.path), - }, - }, {signal}); - - const documentId = uploaded?.id ?? uploaded?.document_id; - if (!documentId) { - throw new Error(Environment.getMistralUploadedDocumentIdMissingText(doc.fileName)); - } - - let processed = false; - for (let i = 0; i < 90; i++) { - const info: any = await mistralAi.beta.libraries.documents.status({libraryId, documentId}, {signal}); - const status = info.status ?? info.process_status ?? info.processing_status; - - if (status === "processed" || status === "Completed" || status === "done" || status === "Done") { - processed = true; - break; - } - if (status === "failed" || status === "error" || status === "Failed" || status === "Error" || status === "missing_content") { - throw new Error(Environment.getMistralDocumentProcessingFailedText(doc.fileName, status)); - } - await delay(2000, signal); - } - - if (!processed) { - throw new Error(Environment.getMistralDocumentProcessingTimedOutText(doc.fileName)); - } - - result.push({type: "file", id: libraryId}); - } - - return {documents: result, libraryId}; - } catch (e) { - await deleteMistralLibrary(libraryId, target); - throw e; - } -} - -async function executeTool( - toolCall: ToolCallData, - message: TelegramStreamMessage, - context: ToolRuntimeContext, -): Promise { - await message.flush(); - const result = await executeToolCall(toolCall.name, safeJsonParseObject(toolCall.argumentsText), context); - await sendToolArtifacts(toolCall, result, message); - return result; -} - -function toolResourceKeys(toolCall: ToolCallData): string[] { - const args = safeJsonParseObject(toolCall.argumentsText); - const pathValue = typeof args.path === "string" ? args.path : undefined; - const sourcePath = typeof args.sourcePath === "string" ? args.sourcePath : undefined; - const targetPath = typeof args.targetPath === "string" ? args.targetPath : undefined; - - switch (toolCall.name) { - case "get_datetime": - case "web_search": - case "get_weather": - case "read_file": - case "list_directory": - return []; - case "create_file": - case "create_directory": - case "update_file": - case "delete_path": - return [`file:${pathValue ?? "*"}`]; - case "copy_path": - case "rename_path": - return [`file:${sourcePath ?? "*"}`, `file:${targetPath ?? "*"}`]; - case "shell_execute": - return ["shell:*"]; - default: - return [`tool:${toolCall.name}`]; - } -} - -async function runWithToolLocks(keys: string[], task: () => Promise): Promise { - const uniqueKeys = [...new Set(keys)].sort(); - const run = (index: number): Promise => { - const key = uniqueKeys[index]; - if (!key) return task(); - return toolResourceLocks.runExclusive(key, () => run(index + 1)); - }; - - return run(0); -} - -async function executeScheduledTool( - toolCall: ToolCallData, - message: TelegramStreamMessage, - context: ToolRuntimeContext, -): Promise { - const keys = toolResourceKeys(toolCall); - if (!keys.length) return executeTool(toolCall, message, context); - return runWithToolLocks(keys, () => executeTool(toolCall, message, context)); -} - -async function executeToolBatch( - toolCalls: ToolCallData[], - message: TelegramStreamMessage, - context: ToolRuntimeContext, -): Promise { - if (!toolCalls.length) return []; - - message.setStatus(Environment.getUseToolText(toolCalls)); - await message.flush(); - - const result = await Promise.all(toolCalls.map(async toolCall => { - message.setStatus(Environment.getUseToolText(toolCalls)); - await message.flush(); - const result = await executeScheduledTool(toolCall, message, context); - message.setStatus(Environment.getUseToolText(toolCalls)); - await message.flush(); - return stringifyToolExecutionResult(result); - })); - message.setStatus(Environment.getUseToolText(toolCalls)); - await message.flush(); - - return result; -} - -function appendOllamaToolResults(messages: ChatMessage[], calls: ToolCallData[], results: string[]): void { - for (const [index, call] of calls.entries()) { - messages.push({ - role: "tool", - content: results[index] ?? "", - tool_name: call.name - }); - } -} - -function stringifyToolExecutionResult(result: unknown): string { - return typeof result === "string" ? result : JSON.stringify(result); -} - -async function appendMistralToolResult( - messages: MistralChatMessage[], - call: ToolCallData, - streamMessage: TelegramStreamMessage, - context: ToolRuntimeContext, -): Promise { - const result = await executeTool(call, streamMessage, context); - messages.push({ - role: "tool", - name: call.name, - toolCallId: call.id, - content: stringifyToolExecutionResult(result), - }); -} - -function getOpenAIResponsesToolsWithImage(config: RuntimeConfigSnapshot): any[] { - return [ - ...getOpenAIResponsesTools(), - { - type: "image_generation", - model: config.openAiImageTarget.model, - size: "auto", - moderation: "low", - output_format: "png", - partial_images: OPENAI_IMAGE_PARTIALS, - }, - ]; -} - -function collectOpenAiResponseText(response: any): string { - if (typeof response?.output_text === "string") return response.output_text; - - return (response?.output ?? []) - .filter((item: any) => item?.type === "message") - .flatMap((item: any) => item.content ?? []) - .map((content: any) => content?.text ?? content?.refusal ?? "") - .join(""); -} - -function collectOpenAiResponseFunctionCalls(response: any): OpenAiResponsesFunctionCall[] { - return (response?.output ?? []) - .filter((item: any) => item?.type === "function_call" && item.call_id && item.name) - .map((item: any) => ({ - callId: item.call_id, - name: item.name, - argumentsText: item.arguments ?? "{}", - })); -} - -function collectOpenAiResponseImages(response: any): string[] { - return (response?.output ?? []) - .filter((item: any) => item?.type === "image_generation_call" && item.result) - .map((item: any) => item.result); -} - -function writeOpenAiGeneratedImage(sourceMessage: Message, b64: string, label: string): Buffer { - const imageBuffer = Buffer.from(b64, "base64"); - const fileName = `${sourceMessage.chat.id}_${sourceMessage.message_id}_${Date.now()}_${label}.png`; - fs.writeFileSync(path.join(photoGenDir, fileName), imageBuffer); - return imageBuffer; -} - -async function showOpenAiGeneratedImage( - streamMessage: TelegramStreamMessage, - sourceMessage: Message, - b64: string, - label: string, - status: string, - final: boolean, -): Promise { - const imageBuffer = writeOpenAiGeneratedImage(sourceMessage, b64, label); - if (final && !streamMessage.getText().trim()) { - streamMessage.replaceText(status); - streamMessage.clearStatus(); - } else { - streamMessage.setStatus(status); - } - await streamMessage.showImage(imageBuffer); -} - -async function runOpenAi( - messages: OpenAIChatMessage[], - streamMessage: TelegramStreamMessage, - signal: AbortSignal, - stream: boolean, - firstRoundStatus: string, - sourceMessage: Message, - config: RuntimeConfigSnapshot, - toolContext: ToolRuntimeContext, -): Promise { - firstRoundStatus; - let responseInput = [...messages]; - const openAi = createOpenAiClient(config.openAiChatTarget); - - for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { - // const status = roundStatus(round, firstRoundStatus); - // if (!status) { - // streamMessage.clearStatus(); - // } else { - // streamMessage.setStatus(status); - // } - - // streamMessage.setStatus(roundStatus(round, firstRoundStatus)); - // await streamMessage.flush(); - - if (!stream) { - const response = await openAi.responses.create({ - model: config.openAiChatTarget.model, - input: responseInput, - tools: getOpenAIResponsesToolsWithImage(config), - parallel_tool_calls: true, - // instructions: systemPrompt, - }, {signal}); - - streamMessage.append(collectOpenAiResponseText(response)); - const images = collectOpenAiResponseImages(response); - if (images.length) { - await showOpenAiGeneratedImage( - streamMessage, - sourceMessage, - images[images.length - 1], - `final_${round}`, - Environment.getImageGenDoneText(config.openAiImageTarget.model), - true, - ); - } - - const calls = collectOpenAiResponseFunctionCalls(response); - if (!calls.length) return; - - const toolOutputs = []; - for (const call of calls) { - const result = await executeTool({ - id: call.callId, - name: call.name, - argumentsText: call.argumentsText - }, streamMessage, toolContext); - toolOutputs.push({ - type: "function_call_output", - call_id: call.callId, - output: typeof result === "string" ? result : JSON.stringify(result), - }); - } - // @ts-ignore - messages = [...messages, ...(response.output ?? []), ...toolOutputs]; - responseInput = [...messages]; - continue; - } - - let completedResponse: any = null; - const response = await openAi.responses.create({ - model: config.openAiChatTarget.model, - input: responseInput, - stream: true, - tools: getOpenAIResponsesToolsWithImage(config), - parallel_tool_calls: true, - }, {signal}); - - - let localToolCalls: ToolCallData[] = []; - for await (const event of response) { - if (signal.aborted) throw new Error("Aborted"); - - switch (event.type) { - case "response.output_text.delta": - streamMessage.append(event.delta ?? ""); - break; - case "response.image_generation_call.in_progress": - streamMessage.setStatus(Environment.startingImageGenText); - await streamMessage.flush(); - break; - case "response.image_generation_call.generating": - streamMessage.setStatus(Environment.imageGenText); - await streamMessage.flush(); - break; - case "response.image_generation_call.partial_image": { - const iteration = (event.partial_image_index ?? 0) + 1; - await showOpenAiGeneratedImage( - streamMessage, - sourceMessage, - event.partial_image_b64, - `partial_${round}_${iteration}`, - Environment.getPartialImageGenText(iteration, OPENAI_IMAGE_PARTIALS), - false, - ); - break; - } - case "response.image_generation_call.completed": - streamMessage.setStatus(Environment.finalizingImageGenText); - await streamMessage.flush(); - break; - case "response.output_item.added": - if (event.item.type === "function_call" && event.item.name) { - localToolCalls.push({ - id: event.item.id, - name: event.item.name, - argumentsText: event.item.arguments, - }); - - streamMessage.setStatus(Environment.getUseToolText(localToolCalls)); - await streamMessage.flush(); - } - break; - case "response.output_item.done": - if (event.item.type === "function_call" && event.item.name) { - const index = localToolCalls.findIndex(c => c.id === event.item.id); - if (index !== -1) { - localToolCalls.splice(index, 1); - if (localToolCalls.length === 0) { - streamMessage.clearStatus(); - } else { - streamMessage.setStatus(Environment.getUseToolText(localToolCalls)); - } - await streamMessage.flush(); - } - } - break; - case "response.function_call_arguments.delta": - break; - case "response.function_call_arguments.done": - break; - - case "response.completed": - completedResponse = event.response; - break; - case "response.failed": - throw new Error(event.response?.error?.message ?? "OpenAI response failed"); - case "error": - throw new Error(event.message ?? event?.message ?? "OpenAI stream error"); - } - } - - if (!completedResponse) throw new Error("OpenAI did not return the final response.completed event."); - - const images = collectOpenAiResponseImages(completedResponse); - if (images.length) { - await showOpenAiGeneratedImage( - streamMessage, - sourceMessage, - images[images.length - 1], - `final_${round}`, - Environment.getImageGenDoneText(config.openAiImageTarget.model), - true, - ); - } - - const calls = collectOpenAiResponseFunctionCalls(completedResponse); - if (!calls.length) return; - - const toolOutputs = []; - for (const call of calls) { - const result = await executeTool({ - id: call.callId, - name: call.name, - argumentsText: call.argumentsText - }, streamMessage, toolContext); - toolOutputs.push({ - type: "function_call_output", - call_id: call.callId, - output: typeof result === "string" ? result : JSON.stringify(result), - }); - } - responseInput = [...responseInput, ...(completedResponse.output ?? []), ...toolOutputs]; - } -} - -async function runOllama( - msg: Message, - messages: ChatMessage[], - streamMessage: TelegramStreamMessage, - signal: AbortSignal, - stream: boolean, - think: Think, - firstRoundStatus: string, - config: RuntimeConfigSnapshot, - toolContext: ToolRuntimeContext, - contextSize?: number, -): Promise { - const fromId = msg.from?.id; - - const audioCount = messages.filter(m => m.audios?.length).flatMap(m => m.audios ?? []).length; - const videoNoteCount = messages.filter(m => m.videoNotes?.length).flatMap(m => m.videoNotes ?? []).length; - const imageCount = messages.filter(m => m.images?.length).flatMap(m => m.images ?? []).length; - - const target = (audioCount || videoNoteCount) ? config.ollamaAudioTarget : - imageCount ? config.ollamaVisionTarget : - think ? config.ollamaThinkingTarget : config.ollamaChatTarget; - const model = target.model; - const ollama = createOllamaClient(target); - const modelInfo = await ollama.show({model: model}); - const contextKey = Object.keys(modelInfo.model_info).find(k => k.endsWith(".context_length")); - // @ts-ignore - const maxContextLength = contextKey ? modelInfo?.model_info?.[contextKey] : DEFAULT_OLLAMA_CONTEXT_SIZE; - const context = clamp(contextSize ?? contextSize === -1 ? MAX_OLLAMA_CONTEXT_SIZE : DEFAULT_OLLAMA_CONTEXT_SIZE, MIN_OLLAMA_CONTEXT_SIZE, maxContextLength ?? DEFAULT_OLLAMA_CONTEXT_SIZE); - - const modelsToLoad = [model]; - - try { - const activeModels = (await ollama.ps()).models.map(m => m.model); - const oldSet = new Set(activeModels); - const newSet = new Set(modelsToLoad); - - const added = modelsToLoad.filter(m => !oldSet.has(m)); - const removed = activeModels.filter(m => !newSet.has(m)); - const diff = [...added, ...removed]; - if (diff.length) { - await unloadAllOllamaModels(ollama, modelsToLoad); - } - } catch (e) { - logError(e); - } - - if (!(await isOllamaModelActive(ollama, target))) { - const currentStatus = streamMessage.getStatus(); - streamMessage.setStatus(Environment.getLoadingModelText(model)); - await streamMessage.flush(); - if (await loadOllamaModel(model, ollama, context)) { - streamMessage.setStatus(currentStatus ?? Environment.waitThinkText); - await streamMessage.flush(); - } - } - - // console.log("STREAM_MESSAGE", JSON.stringify(streamMessage)); - - let interval: NodeJS.Timeout | null = null; - - if (!stream) { - let typingInFlight = false; - const applyTyping = async () => { - if (typingInFlight) return; - typingInFlight = true; - try { - await enqueueTelegramApiCall( - () => bot.sendChatAction({chat_id: msg.chat.id, action: "typing"}), - {method: "sendChatAction", chatId: msg.chat.id, chatType: msg.chat.type} - ).catch(logError); - } finally { - typingInFlight = false; - } - }; - - await applyTyping(); - interval = setInterval(() => { - applyTyping().catch(logError); - }, 5000); - } - - try { - for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { - console.log("CONTEXT_LENGTH", context); - - const request: ChatRequest = { - model: model, - messages: messages, - think: audioCount ? false : think, - options: { - temperature: messages.length <= 2 ? 0.6 : 0.6, - num_ctx: context, - } - }; - - if ((await getModelCapabilities(AiProvider.OLLAMA, model, "tools"))?.tools?.supported) { - const ollamaTools: Tool[] = fromId !== Environment.CREATOR_ID ? - [ - getCurrentDateTimeTool, - getMarketRatesTool, - getWeatherTool - ] : getOllamaTools() as Tool[]; - - if (!config.ollamaToolRankerTarget) { - request.tools = ollamaTools; - } else { - try { - const toolRankerPrompt = config.rankerToolPrompt - ?.replace( - "{{user_request}}", - messages.reverse().find(m => m.role === "user")?.content ?? "" - ) - ?.replace( - "{{tools}}", - ollamaTools.map(t => JSON.stringify(t)).join("\n") - ); - - streamMessage.setStatus("🦽 Подбираю лучшие инструменты..."); - await streamMessage.flush(); - - const client = createOllamaClient(config.ollamaToolRankerTarget); - - const modelsToLoad = [config.ollamaToolRankerTarget.model]; - - const activeModels = (await client.ps()).models.map(m => m.model); - const oldSet = new Set(activeModels); - const newSet = new Set(modelsToLoad); - - const added = modelsToLoad.filter(m => !oldSet.has(m)); - const removed = activeModels.filter(m => !newSet.has(m)); - const diff = [...added, ...removed]; - if (diff.length) { - await unloadAllOllamaModels(client, modelsToLoad); - } - - const result = await client.generate({ - model: config.ollamaToolRankerTarget.model, - stream: false, - prompt: toolRankerPrompt ?? "" - }); - - console.log("TOOL_RANKER_RESULT: ", result.response); - - const raw = JSON.parse(result.response); - const res = RouterPlanSchema.safeParse(raw); - if (res.success) { - const toolNames = res.data.s.flatMap(s => s.t); - const tools = toolNames.map(n => ollamaTools.find(t => t.function.name === n) as Tool); - // const tools = ollamaTools.filter(t => toolNames.includes(t.function.name ?? "")); - request.tools = tools; - - - const m = messages.reverse().find(m => m.role === "user"); - let content = m?.content ?? ""; - content += "\n" + JSON.stringify({ - toolPlan: { - steps: res.data.s.map(s => { - return { - tool_name: s.t, - hint: s.h, - from: s.from, - }; - }), - missing_info: res.data.m - } - }); - - if (m) m.content = content; - // messages.reverse().find(m => m.role === "user")?.content += ""; - // messages[messages.length - 1].content = content; - } - } catch (e: unknown) { - logError(e); - } - } - } - - if (!stream) { - const response = await ollama.chat({ - ...request, - stream: false - }); - - const message = response.message; - - streamMessage.append(message?.content ?? ""); - - const calls = (message?.tool_calls ?? []).map((c: any, i: number) => ({ - id: `ollama_${round}_${i}`, - name: c.function?.name, - argumentsText: JSON.stringify(c.function?.arguments ?? {}) - })); - - if (!calls.length) break; - messages.push({ - role: "assistant", - content: message?.content ?? "", - tool_calls: calls.map(c => ({ - function: { - name: c.name, - arguments: safeJsonParseObject(c.argumentsText), - }, - })), - }); - - appendOllamaToolResults(messages, calls, await executeToolBatch(calls, streamMessage, toolContext)); - continue; - } - - const response = await ollama.chat({ - ...request, - stream: true - }); - - const calls: ToolCallData[] = []; - const abortOllamaResponse = () => response.abort?.(); - signal.addEventListener("abort", abortOllamaResponse, {once: true}); - if (signal.aborted) abortOllamaResponse(); - try { - for await (const chunk of response) { - const localToolCalls: ToolCallData[] = []; - - if (chunk.message.tool_calls) { - for (const [i, call] of (chunk.message?.tool_calls ?? []).entries()) { - localToolCalls.push({ - id: `ollama_${round}_${i}`, - name: call.function?.name, - argumentsText: JSON.stringify(call.function?.arguments ?? {}) - }); - } - } - - const newStatus = roundStatus(round, firstRoundStatus, chunk.message.content, localToolCalls, !!chunk.message.thinking); - const previousStatus = streamMessage.getStatus(); - if (newStatus && newStatus !== Environment.waitThinkText) { - streamMessage.setStatus(newStatus); - } else { - streamMessage.clearStatus(); - } - - if (streamMessage.getStatus() !== previousStatus && previousStatus && newStatus !== Environment.waitThinkText) { - await streamMessage.flush(); - } - - // console.log("OLLAMA_CHUNK", JSON.stringify(chunk)); - - if (signal.aborted) { - response.abort?.(); - throw new Error("Aborted"); - } - - if (chunk.message?.thinking && streamMessage.getStatus() !== Environment.reasoningText) { - // streamMessage.setStatus(Environment.reasoningText); - // await streamMessage.flush(); - } else { - // if (!streamMessage.getText().trim().length) { - // if (streamMessage.getStatus() === Environment.reasoningText) { - // streamMessage.clearStatus(); - // await streamMessage.flush(); - // } else if (streamMessage.getStatus() !== Environment.waitThinkText) { - // streamMessage.setStatus(Environment.waitThinkText); - // await streamMessage.flush(); - // } - // } else { - // if (streamMessage.getStatus().length) { - // streamMessage.clearStatus(); - // await streamMessage.flush(); - // } - // } - - streamMessage.append(chunk.message?.content ?? ""); - } - - for (const [i, call] of (chunk.message?.tool_calls ?? []).entries()) { - calls.push({ - id: `ollama_${round}_${i}`, - name: call.function?.name, - argumentsText: JSON.stringify(call.function?.arguments ?? {}) - }); - } - - if (chunk.done) { - await streamMessage.flush(streamMessage.regenerateKeyboard(), true); - } - } - } finally { - signal.removeEventListener("abort", abortOllamaResponse); - } - if (!calls.length) break; - - messages.push({ - role: "assistant", - content: streamMessage.getText(), - tool_calls: calls.map(c => ({ - function: { - name: c.name, - arguments: safeJsonParseObject(c.argumentsText) - } - })) - }); - - const toolResults = await executeToolBatch(calls, streamMessage, toolContext); - - let successGetNoteFileResult: GetNoteFileResult | undefined = undefined; - - for (const toolResult of toolResults) { - try { - const raw = JSON.parse(toolResult); - const res = GetNoteFileResultSchema.safeParse(raw); - if (res.success && res.data.success) { - successGetNoteFileResult = res.data; - } - } catch (e: unknown) { - await replyToMessage({message: msg, text: e + ""}).catch(logError); - } - } - - if (successGetNoteFileResult) { - await bot.sendDocument({ - chat_id: msg.chat.id, - reply_parameters: { - message_id: msg.message_id - }, - document: createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)) - }).catch(logError); - } - - const reversedMessages = messages.reverse(); - let lastUserMessageIndex = reversedMessages.findIndex(m => m.role === "user"); - if (lastUserMessageIndex >= 0) { - lastUserMessageIndex = messages.indexOf(reversedMessages[lastUserMessageIndex]); - if (lastUserMessageIndex >= 0) { - messages[lastUserMessageIndex].content += "\n\nLast tool_call result: " + JSON.stringify(toolResults); - } - } - - appendOllamaToolResults(messages, calls, toolResults); - } - } finally { - if (interval) clearInterval(interval); - } -} - -async function runMistral( - messages: MistralChatMessage[], - documents: any[], - streamMessage: TelegramStreamMessage, - signal: AbortSignal, - stream: boolean, - firstRoundStatus: string, - config: RuntimeConfigSnapshot, - toolContext: ToolRuntimeContext, -): Promise { - documents; - const mistralAi = createMistralClient(config.mistralChatTarget); - for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { - if (signal.aborted) throw new Error("Aborted"); - - streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? ""); - await streamMessage.flush(); - - if (!stream) { - const response = await mistralAi.chat.complete({ - model: config.mistralChatTarget.model, - messages: messages as any, - tools: getMistralTools() as any, - // documents: documents as any - }, {signal}); - const msg = response.choices?.[0]?.message; - streamMessage.append(typeof msg?.content === "string" ? msg.content : JSON.stringify(msg?.content ?? "")); - const calls = normalizeMistralToolCalls(msg?.toolCalls ?? []); - if (!calls.length) return; - messages.push({ - role: "assistant", - content: (msg?.content ?? "") as any, - toolCalls: msg?.toolCalls ?? [], - }); - for (const call of calls) await appendMistralToolResult(messages, call, streamMessage, toolContext); - continue; - } - - const streamResponse = await mistralAi.chat.stream({ - model: config.mistralChatTarget.model, - messages: messages as any, - tools: getMistralTools() as any, - // documents: documents as any - }, {signal}); - let calls: ToolCallData[] = []; - for await (const event of streamResponse) { - if (signal.aborted) throw new Error("Aborted"); - const choice = event.data?.choices?.[0]; - const delta = choice?.delta; - streamMessage.append(contentFromMistralDelta(delta)); - const deltaCalls = normalizeMistralToolCalls(delta?.toolCalls ?? []); - if (deltaCalls.length) calls = deltaCalls; - } - if (!calls.length) return; - messages.push({ - role: "assistant", - content: streamMessage.getText(), - toolCalls: calls.map(c => ({id: c.id, function: {name: c.name, arguments: c.argumentsText}})) - }); - for (const call of calls) await appendMistralToolResult(messages, call, streamMessage, toolContext); - } -} - -function openAiResponseContentToText(content: unknown): string { - if (typeof content === "string") return content; - if (!Array.isArray(content)) return ""; - return content.map((part: any) => part?.text ?? part?.content ?? part?.refusal ?? "").join(""); -} - -function openAiResponseMessagesToChatCompletions(messages: OpenAIChatMessage[]): any[] { - return messages.map((message: any) => { - if (message.role === "system" || message.role === "assistant") { - return { - role: message.role, - content: openAiResponseContentToText(message.content), - }; - } - - const content = Array.isArray(message.content) - ? message.content.map((part: any) => { - if (part.type === "input_image") { - return { - type: "image_url", - image_url: {url: part.image_url}, - }; - } - - return { - type: "text", - text: part.text ?? "", - }; - }) - : message.content; - - return {role: "user", content}; - }); -} - -function normalizeOpenAiChatToolCalls(toolCalls: any[] = []): ToolCallData[] { - return toolCalls.map((call, i) => ({ - id: call.id || `openai_chat_${Date.now()}_${i}`, - name: call.function?.name || call.name || "", - argumentsText: typeof call.function?.arguments === "string" - ? call.function.arguments - : JSON.stringify(call.function?.arguments ?? call.arguments ?? {}), - })).filter(call => call.name); -} - -function collectOpenAiChatStreamToolCalls(toolCalls: any[] = []): ToolCallData[] { - const byIndex = new Map(); - - for (const item of toolCalls) { - const index = item.index ?? byIndex.size; - const existing = byIndex.get(index) ?? { - id: item.id || `openai_chat_stream_${Date.now()}_${index}`, - name: "", - argumentsText: "", - }; - - if (item.id) existing.id = item.id; - if (item.function?.name) existing.name = item.function.name; - if (item.function?.arguments) existing.argumentsText += item.function.arguments; - byIndex.set(index, existing); - } - - return [...byIndex.values()].filter(call => call.name); -} - -async function appendOpenAiChatToolResults( - messages: any[], - calls: ToolCallData[], - results: string[], -): Promise { - for (const [index, call] of calls.entries()) { - messages.push({ - role: "tool", - tool_call_id: call.id, - content: results[index] ?? "", - }); - } -} - -async function runOpenAiCompatibleChat( - messages: OpenAIChatMessage[], - streamMessage: TelegramStreamMessage, - signal: AbortSignal, - stream: boolean, - firstRoundStatus: string, - config: RuntimeConfigSnapshot, - toolContext: ToolRuntimeContext, -): Promise { - const geminiOpenAi = createGeminiOpenAiClient(config.geminiChatTarget); - const chatMessages = openAiResponseMessagesToChatCompletions(messages); - - for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { - streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? ""); - await streamMessage.flush(); - - if (!stream) { - const response: any = await geminiOpenAi.chat.completions.create({ - model: config.geminiChatTarget.model, - messages: chatMessages, - tools: getOpenAITools() as any, - parallel_tool_calls: true, - temperature: chatMessages.length <= 2 ? 0 : 0.6, - }, {signal}); - const message = response.choices?.[0]?.message; - streamMessage.append(message?.content ?? ""); - const calls = normalizeOpenAiChatToolCalls(message?.tool_calls ?? []); - if (!calls.length) return; - - chatMessages.push({ - role: "assistant", - content: message?.content ?? "", - tool_calls: message?.tool_calls ?? [], - }); - await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext)); - continue; - } - - const response: any = await geminiOpenAi.chat.completions.create({ - model: config.geminiChatTarget.model, - messages: chatMessages, - tools: getOpenAITools() as any, - parallel_tool_calls: true, - temperature: chatMessages.length <= 2 ? 0 : 0.6, - stream: true, - }, {signal}); - - const streamToolCalls: any[] = []; - for await (const chunk of response) { - if (signal.aborted) throw new Error("Aborted"); - const delta = chunk.choices?.[0]?.delta; - streamMessage.append(delta?.content ?? ""); - if (delta?.tool_calls?.length) { - streamToolCalls.push(...delta.tool_calls); - } - } - - const calls = collectOpenAiChatStreamToolCalls(streamToolCalls); - if (!calls.length) return; - - chatMessages.push({ - role: "assistant", - content: streamMessage.getText(), - tool_calls: calls.map(call => ({ - id: call.id, - type: "function", - function: { - name: call.name, - arguments: call.argumentsText, - }, - })), - }); - await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext)); - } -} - -function collectGeminiResponseText(response: any): string { - if (typeof response?.text === "string") return response.text; - - return (response?.candidates ?? []) - .flatMap((candidate: any) => candidate?.content?.parts ?? []) - .map((part: any) => part?.text ?? "") - .join(""); -} - -function collectGeminiFunctionCalls(response: any): ToolCallData[] { - const calls = response?.functionCalls - ?? (response?.candidates ?? []).flatMap((candidate: any) => { - return (candidate?.content?.parts ?? []) - .map((part: any) => part?.functionCall) - .filter(Boolean); - }); - - return (calls ?? []).map((call: any, index: number) => ({ - id: call.id || `gemini_${Date.now()}_${index}`, - name: call.name ?? "", - argumentsText: JSON.stringify(call.args ?? call.arguments ?? {}), - })).filter((call: ToolCallData) => call.name); -} - -function mergeGeminiFunctionCalls(existing: ToolCallData[], next: ToolCallData[]): ToolCallData[] { - const merged = [...existing]; - for (const call of next) { - const index = merged.findIndex(item => item.id === call.id); - if (index === -1) { - merged.push(call); - } else { - merged[index] = call; - } - } - return merged; -} - -function appendGeminiToolRound(messages: GeminiMessage[], calls: ToolCallData[], results: string[]): void { - messages.push({ - role: "model", - parts: calls.map(call => ({ - functionCall: { - id: call.id, - name: call.name, - args: safeJsonParseObject(call.argumentsText), - }, - })), - }); - - messages.push({ - role: "user", - parts: calls.map((call, index) => ({ - functionResponse: { - id: call.id, - name: call.name, - response: {result: results[index] ?? ""}, - }, - })), - }); -} - -async function runGemini( - messages: GeminiMessage[], - streamMessage: TelegramStreamMessage, - signal: AbortSignal, - stream: boolean, - firstRoundStatus: string, - config: RuntimeConfigSnapshot, - toolContext: ToolRuntimeContext, -): Promise { - const geminiAi = createGoogleGenAiClient(config.geminiChatTarget); - - for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { - if (signal.aborted) throw new Error("Aborted"); - - streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? ""); - await streamMessage.flush(); - - const request = { - model: config.geminiChatTarget.model, - contents: messages as any, - config: { - tools: getGeminiTools() as any, - temperature: messages.length <= 2 ? 0 : 0.6, - abortSignal: signal, - }, - }; - - if (!stream) { - const response: any = await geminiAi.models.generateContent(request); - streamMessage.append(collectGeminiResponseText(response)); - const calls = collectGeminiFunctionCalls(response); - if (!calls.length) return; - - appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext)); - continue; - } - - const response: any = await geminiAi.models.generateContentStream(request); - let calls: ToolCallData[] = []; - for await (const chunk of response) { - if (signal.aborted) throw new Error("Aborted"); - streamMessage.append(collectGeminiResponseText(chunk)); - calls = mergeGeminiFunctionCalls(calls, collectGeminiFunctionCalls(chunk)); - } - - if (!calls.length) return; - appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext)); - } -} +export type {ToolCallData} from "./unified-ai-runner.shared"; +export {snapshotModel, providerTargets, ollamaModelNames} from "./unified-ai-runner.shared"; async function executeUnifiedAiRequest( options: UnifiedRunOptions, @@ -2081,6 +34,23 @@ async function executeUnifiedAiRequest( controller: AbortController, streamMessage: TelegramStreamMessage, ): Promise<{ mistralLibraryId?: string }> { + const requestStartedAt = Date.now(); + aiLog("info", "request.execute.start", { + provider: providerName(options.provider), + stream: options.stream ?? true, + think: options.think, + responseLanguage: options.responseLanguage, + contextSize: options.contextSize, + voiceMode: options.voiceMode, + message: aiLogMessageIdentity(options.msg), + downloads: downloads.map(d => ({ + kind: d.kind, + fileName: d.fileName, + mimeType: d.mimeType, + sizeBytes: d.buffer.length + })), + }); + const { chatMessages, imageCount @@ -2094,12 +64,20 @@ async function executeUnifiedAiRequest( ); const firstRoundStatus = initialStatus(downloads, imageCount); const toolContext = toolRuntimeContextFromDownloads(downloads); + aiLog("debug", "request.messages.collected", { + provider: providerName(options.provider), + chatMessages: chatMessages.length, + imageCount, + firstRoundStatus, + hasToolInputFiles: !!toolContext.pythonInputFiles?.length, + }); streamMessage.setStatus(firstRoundStatus); await streamMessage.flush(); const hasDocument = downloads.some(d => d.kind === "document"); if (hasDocument && options.provider !== AiProvider.MISTRAL && options.provider !== AiProvider.OLLAMA) { + aiLog("warn", "request.documents.unsupported_provider", {provider: providerName(options.provider)}); throw new Error(Environment.documentsUnifiedRunnerUnsupportedText); } @@ -2112,7 +90,7 @@ async function executeUnifiedAiRequest( if (transcript.trim()) { if (options.voiceMode === AI_VOICE_MODE_TRANSCRIPT) { - // TODO: 12.05.2026, Danil Nikolaev: extract to string + // TODO: 12.05.2026: extract to string streamMessage.replaceText(`[Расшифровка]\n${transcript.trim()}`); await streamMessage.finish(); return {mistralLibraryId}; @@ -2120,11 +98,16 @@ async function executeUnifiedAiRequest( appendTranscriptToChatMessages(chatMessages, options.provider, transcript); stripAudioFromRunnerMessages(chatMessages); + aiLog("debug", "request.transcript.appended", { + provider: providerName(options.provider), + transcriptChars: transcript.length, + chatMessages: chatMessages.length, + }); } try { const preparedMistral = options.provider === AiProvider.MISTRAL - ? await prepareMistralDocuments(downloads, chatMessages, streamMessage, config.mistralChatTarget, controller.signal) + ? await prepareMistralDocuments(downloads, chatMessages as MistralChatMessage[], streamMessage, config.mistralChatTarget, controller.signal) : {documents: []}; const documents = preparedMistral.documents; mistralLibraryId = preparedMistral.libraryId; @@ -2150,6 +133,8 @@ async function executeUnifiedAiRequest( }); } + aiLog("info", "request.provider.dispatch", {provider: providerName(options.provider)}); + switch (options.provider) { case AiProvider.OPENAI: await runOpenAi(chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, options.msg, config, toolContext); @@ -2182,8 +167,19 @@ async function executeUnifiedAiRequest( await streamMessage.finish(); // await sendVoiceResponseIfNeeded(options, downloads, streamMessage.getText()); + aiLog("success", "request.execute.done", { + provider: providerName(options.provider), + duration: aiLogDuration(requestStartedAt), + responseChars: streamMessage.getText().length, + mistralLibraryId, + }); return {mistralLibraryId}; } catch (e) { + aiLog("error", "request.execute.failed", { + provider: providerName(options.provider), + duration: aiLogDuration(requestStartedAt), + error: e, + }); if (mistralLibraryId) { await deleteMistralLibrary(mistralLibraryId, config.mistralChatTarget); } @@ -2192,22 +188,49 @@ async function executeUnifiedAiRequest( } export async function runUnifiedAi(options: UnifiedRunOptions): Promise { + const startedAt = Date.now(); const config = snapshotRuntimeConfig(); options.responseLanguage ??= await resolveAiResponseLanguageForUser(options.msg.from?.id); options.contextSize ??= await resolveAiContextSizeForUser(options.msg.from?.id); options.voiceMode ??= await resolveAiVoiceModeForUser(options.msg.from?.id); const requestedAttachmentKinds = await collectRequestedAttachmentKinds(options.msg); + aiLog("info", "run.start", { + provider: providerName(options.provider), + model: snapshotModel(options.provider, config), + message: aiLogMessageIdentity(options.msg), + targetMessage: aiLogMessageIdentity(options.targetMessage), + isGuestMsg: options.isGuestMsg, + stream: options.stream, + think: options.think, + responseLanguage: options.responseLanguage, + contextSize: options.contextSize, + voiceMode: options.voiceMode, + requestedAttachmentKinds: [...requestedAttachmentKinds], + textChars: options.text.length, + }); + if (await rejectUnsupportedAttachments(options.provider, snapshotModel(options.provider, config), options.msg, config, requestedAttachmentKinds)) { + aiLog("warn", "run.rejected.unsupported_attachment", { + provider: providerName(options.provider), + requestedAttachmentKinds: [...requestedAttachmentKinds], + }); return; } const cached = await collectCachedMessageAttachments(options.msg); + aiLog("debug", "run.attachments.cache", { + attachments: cached.attachments.map(a => ({kind: a.kind, fileName: a.fileName, cachePath: a.cachePath})), + missing: cached.missing.map(a => ({kind: a.kind, fileName: a.fileName, cachePath: a.cachePath})), + }); if (cached.missing.length) { await replyToMessage({ message: options.msg, text: Environment.getAttachmentMissingFromCacheText(cached.missing[0].fileName), }).catch(logError); + aiLog("warn", "run.rejected.missing_attachment_cache", { + missing: cached.missing.map(a => ({kind: a.kind, fileName: a.fileName, cachePath: a.cachePath})), + }); return; } @@ -2233,32 +256,59 @@ export async function runUnifiedAi(options: UnifiedRunOptions): Promise { cancel.onCancel = () => streamMessage.cancel(cancel.provider); let mistralLibraryId: string | undefined; const queueTarget = resolveAiRequestQueueTarget(options, config, requestedAttachmentKinds); + aiLog("debug", "run.queue.target", {target: aiLogProviderTarget(queueTarget), cancelId: cancel.id}); try { const queueMessage = await streamMessage.start(Environment.getAiQueueText(options.provider, 0)); setAiCancelMessageId(cancel.id, queueMessage.message_id); + aiLog("info", "run.queue.enter", { + cancelId: cancel.id, + queueMessageId: queueMessage.message_id, + target: aiLogProviderTarget(queueTarget), + }); await aiProviderRequestQueue.enqueue(queueTarget, { signal: controller.signal, onPositionChange: async requestsBefore => { + aiLog("debug", "run.queue.position", {cancelId: cancel.id, requestsBefore}); streamMessage.setStatus(Environment.getAiQueueText(options.provider, requestsBefore)); await streamMessage.flush(); }, run: async () => { + const queueWaitFinishedAt = Date.now(); + aiLog("info", "run.queue.dequeued", {cancelId: cancel.id}); const downloads = attachmentsToDownloadedFiles(cached.attachments); + aiLog("debug", "run.downloads.ready", { + count: downloads.length, + downloads: downloads.map(d => ({ + kind: d.kind, + fileName: d.fileName, + mimeType: d.mimeType, + path: d.path, + sizeBytes: d.buffer.length + })), + }); try { const result = await executeUnifiedAiRequest(options, config, downloads, controller, streamMessage); mistralLibraryId = result.mistralLibraryId; + aiLog("success", "run.queue.task.done", { + cancelId: cancel.id, + duration: aiLogDuration(queueWaitFinishedAt), + mistralLibraryId, + }); } finally { cleanupDownloads(downloads); + aiLog("debug", "run.downloads.cleaned", {cancelId: cancel.id, count: downloads.length}); } }, }); - } catch (e: any) { - if (controller.signal.aborted || String(e?.message ?? e).includes("Aborted")) { + } catch (e) { + if (controller.signal.aborted || isAbortError(e)) { + aiLog("warn", "run.aborted", {cancelId: cancel.id, duration: aiLogDuration(startedAt), error: e}); streamMessage.replaceText(streamMessage.getText()); await streamMessage.finish(); } else { + aiLog("error", "run.failed", {cancelId: cancel.id, duration: aiLogDuration(startedAt), error: e}); await streamMessage.fail(e); logError(e); } @@ -2266,7 +316,18 @@ export async function runUnifiedAi(options: UnifiedRunOptions): Promise { clearTimeout(timeout); finishAiRequest(cancel.id); if (mistralLibraryId) { + aiLog("debug", "run.mistral_library.cleanup", {mistralLibraryId}); await deleteMistralLibrary(mistralLibraryId, config.mistralChatTarget); } + aiLog("success", "run.finished", { + cancelId: cancel.id, + provider: providerName(options.provider), + duration: aiLogDuration(startedAt), + aborted: controller.signal.aborted, + }); } } + +export class UnifiedAiRunner { + static run = runUnifiedAi; +} diff --git a/src/callback_commands/ai-cancel.ts b/src/callback_commands/ai-cancel.ts index 2fa7b69..b720ee2 100644 --- a/src/callback_commands/ai-cancel.ts +++ b/src/callback_commands/ai-cancel.ts @@ -96,8 +96,8 @@ export class AiCancel extends CallbackCommand { {method: "editMessageText", chatId: message.chat.id, chatType: message.chat.type} ); - if (result && result !== true) { - await MessageStore.put({...result, text: cancelledText} as Message); + if (result) { + await MessageStore.put({...(result as object), text: cancelledText} as Message); } else { await MessageStore.put({ chatId: message.chat.id, diff --git a/src/commands/ae.ts b/src/commands/ae.ts index 338b3ae..8dba185 100644 --- a/src/commands/ae.ts +++ b/src/commands/ae.ts @@ -21,8 +21,9 @@ export class Ae extends Command { try { let result = this.executeEvaluation(match); await oldSendMessage(msg, result).catch(async () => await errorPlaceholder(msg)); - } catch (e: any) { - const text = e.message.toString(); + } catch (e: unknown) { + const error = e instanceof Error ? e : new Error(String(e)); + const text = error.message.toString(); if (text.includes("is not defined")) { await oldSendMessage(msg, Environment.variableNotDefinedText).catch(logError); @@ -30,7 +31,7 @@ export class Ae extends Command { } logError(`${text} - * Stacktrace: ${e.stack}`); + * Stacktrace: ${error.stack}`); await oldSendMessage(msg, text).catch(logError); } @@ -43,15 +44,16 @@ export class Ae extends Command { e = ((typeof e == "string") ? e : JSON.stringify(e)); return e; - } catch (e: any) { - const text = e.message.toString(); + } catch (e: unknown) { + const error = e instanceof Error ? e : new Error(String(e)); + const text = error.message.toString(); if (text.includes("is not defined")) { return Environment.evaluationVariableNotDefinedText; } logError(`${text} - * Stacktrace: ${e.stack}`); + * Stacktrace: ${error.stack}`); return text; } diff --git a/src/commands/distort.ts b/src/commands/distort.ts index 65b8a22..12455d9 100644 --- a/src/commands/distort.ts +++ b/src/commands/distort.ts @@ -62,7 +62,7 @@ export class Distort extends Command { }), {method: "sendPhoto", chatId, chatType: msg.chat.type} ); - } catch (e: any) { + } catch (e: unknown) { await oldReplyToMessage( msg, Environment.getDistortFailedText(e) ).catch(logError); diff --git a/src/commands/qr.ts b/src/commands/qr.ts index c3ddc75..a226e7f 100644 --- a/src/commands/qr.ts +++ b/src/commands/qr.ts @@ -69,7 +69,7 @@ export class Qr extends Command { }), {method: "sendPhoto", chatId, chatType: msg.chat.type} ); - } catch (e: any) { + } catch (e: unknown) { await replyToMessage({ message: msg, text: Environment.getQrCodeFailedText(e) diff --git a/src/common/environment.ts b/src/common/environment.ts index 259e3bc..10d1c53 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -1636,8 +1636,8 @@ export class Environment { private static getFileMtimeMs(filePath: string): number | undefined { try { return fs.statSync(filePath).mtimeMs; - } catch (e: any) { - if (e?.code === "ENOENT") { + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") { return undefined; } diff --git a/src/common/localization.ts b/src/common/localization.ts index 8da30ed..f1feb35 100644 --- a/src/common/localization.ts +++ b/src/common/localization.ts @@ -23,8 +23,8 @@ function normalizeLanguageCode(value: string | undefined | null): string | undef function readMtimeMs(filePath: string): number | undefined { try { return fs.statSync(filePath).mtimeMs; - } catch (e: any) { - if (e?.code === "ENOENT") return undefined; + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw e; } } diff --git a/src/model/ollama-request.ts b/src/model/ollama-request.ts index 458aeee..0e7d28b 100644 --- a/src/model/ollama-request.ts +++ b/src/model/ollama-request.ts @@ -1,7 +1,6 @@ export type OllamaRequest = { uuid: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - stream: any; + stream: unknown; done: boolean; fromId: number; chatId: number; diff --git a/src/util/shell-command-runner.ts b/src/util/shell-command-runner.ts index fec5b04..20907ae 100644 --- a/src/util/shell-command-runner.ts +++ b/src/util/shell-command-runner.ts @@ -68,11 +68,12 @@ export class ShellCommandRunner { } return {stdout, stderr}; - } catch (error: any) { - console.error("Error code:", error.code); - console.error("Stderr:", error.stderr); + } catch (error: unknown) { + const err = error as Partial; + console.error("Error code:", err.code); + console.error("Stderr:", err.stderr); - return {stdout: error.stdout ?? null, stderr: error.stderr ?? error.message}; + return {stdout: err.stdout ?? null, stderr: err.stderr ?? err.message ?? String(error)}; } } diff --git a/src/util/utils.ts b/src/util/utils.ts index 2da2082..7c40089 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -1,4 +1,5 @@ import * as si from "systeminformation"; +import {redactLogValue} from "../ai/ai-logger"; import {Command} from "../base/command"; import {CallbackCommand} from "../base/callback-command"; import { @@ -65,7 +66,7 @@ export const ignoreIfMarkupFailed = (e: Error | TelegramError) => { }; export const logError = (e: Error | TelegramError | string | unknown) => { - console.error(e); + console.error(redactLogValue(e)); }; export const errorPlaceholder = async (msg: Message) => { @@ -307,13 +308,13 @@ export async function editMessageText(options: EditOptions, retries = 1) { } ); return Promise.resolve(message); - } catch (e: any) { + } catch (e: unknown) { logError(e); - if (isMarkupFailed(e)) { + if (isMarkupFailed(e as Error | TelegramError)) { return Promise.resolve(true); - } else if (isTooManyRequests(e) && retries > 0) { - const retryAfter = Number(e.message.split("retry after ")[1]) || 30; + } else if (isTooManyRequests(e as Error | TelegramError) && retries > 0) { + const retryAfter = Number((e instanceof Error ? e.message : String(e)).split("retry after ")[1]) || 30; await delay(retryAfter * 1000); return editMessageText(options, retries - 1); } else { @@ -1836,9 +1837,10 @@ export function startIntervalEditor(params: { try { await params.editFn(next); lastSent = next; - } catch (e: any) { - if ((e?.description ?? e?.message ?? "").includes("message is not modified")) return; - logError("edit failed: " + e); + } catch (e: unknown) { + const description = e instanceof Error ? e.message : String(e); + if (description.includes("message is not modified")) return; + logError("edit failed: " + description); } }; @@ -1896,7 +1898,6 @@ type RuntimeInfo = | { runtime: "unknown"; version: string }; export function getRuntimeInfo(): RuntimeInfo { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const v = process.versions ?? {}; if (typeof v.bun === "string") { @@ -1906,7 +1907,6 @@ export function getRuntimeInfo(): RuntimeInfo { return {runtime: "node", version: v.node}; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any return {runtime: "unknown", version: String(process.version ?? "")}; } @@ -1957,7 +1957,6 @@ export async function imageToBase64(filePath: string, withMimeType: boolean = fa } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function ifTrue(exp?: string | number | boolean): boolean { if (!exp) return false; diff --git a/tsconfig.json b/tsconfig.json index 56a8f6f..d2f202a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,32 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", // Modern resolution - "rootDir": "src", // Limits scope + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "src", "outDir": "dist", - "incremental": true, // HUGE performance boost - "isolatedModules": true, // Ensures compatibility with fast runners + "incremental": true, + "isolatedModules": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "skipLibCheck": true, - "esModuleInterop": true, "resolveJsonModule": true, "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, + "types": [ + "node", + "bun" + ] }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] // Explicitly exclude build artifacts + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] }