3 Commits

Author SHA1 Message Date
melod1n 674c3cbd44 shitton 2026-05-13 12:05:55 +03:00
melod1n c5b61ee3d8 shitton 2026-05-13 10:18:54 +03:00
melod1n cd8d2683c0 shitton 2026-05-13 05:10:51 +03:00
65 changed files with 5003 additions and 3929 deletions
+18 -161
View File
@@ -28,13 +28,8 @@
"@types/fluent-ffmpeg": "^2.1.28", "@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.6.1", "@types/node": "^25.6.1",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.59.2", "@typescript/native-preview": "^7.0.0-beta",
"@typescript-eslint/parser": "^8.59.2",
"drizzle-kit": "^0.31.10", "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=="], "@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=="], "@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/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=="], "@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/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/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/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=="], "@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=="], "@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/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=="],
"@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=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "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-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=="], "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=="], "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=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "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=="], "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=="], "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="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "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=="], "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=="], "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=="], "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@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"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=="],
"fluent-ffmpeg": ["fluent-ffmpeg@2.1.3", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="], "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-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": ["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-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=="], "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=="], "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-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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "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-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=="], "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=="], "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@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "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=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
@@ -631,8 +512,6 @@
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], "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": ["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=="], "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=="], "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": ["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=="], "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/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/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=="], "@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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "@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=="], "@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=="], "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=="], "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-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=="], "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=="],
} }
} }
+325 -1341
View File
File diff suppressed because it is too large Load Diff
+4 -11
View File
@@ -2,20 +2,18 @@
"name": "tg-chat-bot", "name": "tg-chat-bot",
"main": "src/index.ts", "main": "src/index.ts",
"version": "1.0.0", "version": "1.0.0",
"type": "module",
"scripts": { "scripts": {
"build": "tsc -p tsconfig.build.json", "build": "tsgo -p tsconfig.json",
"lint": "eslint .",
"start": "node dist/index.js", "start": "node dist/index.js",
"bun:start": "bun run dist/index.js" "bun:start": "bun run src/index.ts"
}, },
"dependencies": { "dependencies": {
"@google/genai": "^2.0.0", "@google/genai": "^2.0.0",
"@mistralai/mistralai": "^2.2.1", "@mistralai/mistralai": "^2.2.1",
"openai": "^6.37.0", "openai": "^6.37.0",
"ollama": "^0.6.3", "ollama": "^0.6.3",
"typescript-telegram-bot-api": "^0.16.0", "typescript-telegram-bot-api": "^0.16.0",
"@libsql/client": "^0.17.3", "@libsql/client": "^0.17.3",
"@napi-rs/canvas": "^1.0.0", "@napi-rs/canvas": "^1.0.0",
"axios": "^1.16.0", "axios": "^1.16.0",
@@ -34,12 +32,7 @@
"@types/fluent-ffmpeg": "^2.1.28", "@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.6.1", "@types/node": "^25.6.1",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.59.2",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"eslint": "^10.3.0", "@typescript/native-preview": "^7.0.0-beta"
"tsx": "^4.21.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2"
} }
} }
+1
View File
@@ -0,0 +1 @@
export * from "../logging/ai-logger";
+1 -1
View File
@@ -207,7 +207,7 @@ export function createMistralClient(target: AiRuntimeTarget): Mistral {
export function createOllamaClient(target: AiRuntimeTarget): Ollama { export function createOllamaClient(target: AiRuntimeTarget): Ollama {
return new 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, headers: target.apiKey ? {"Authorization": `Bearer ${target.apiKey}`} : undefined,
}); });
} }
-21
View File
@@ -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; export type AiChatMessage = | OpenAIChatMessage | OllamaChatMessage | MistralChatMessage | GeminiMessage;
+1 -1
View File
@@ -67,7 +67,7 @@ export type MistralContentChunk =
export type MistralFunctionCall = { export type MistralFunctionCall = {
name: string; name: string;
arguments: { [k: string]: any } | string; arguments: Record<string, unknown> | string;
}; };
export type MistralToolCall = { export type MistralToolCall = {
+6 -2
View File
@@ -1,3 +1,7 @@
import {ResponseInputItem} from "openai/resources/responses/responses"; import type {ResponseInputMessageContentList} from "openai/resources/responses/responses";
export type OpenAIChatMessage = ResponseInputItem export type OpenAIChatMessage = {
type: "message";
role: "system" | "user" | "assistant";
content: string | ResponseInputMessageContentList;
};
+21 -8
View File
@@ -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<string[]> { export async function listProviderModels(provider: AiProvider): Promise<string[]> {
const target = resolveAiRuntimeTarget(provider, "chat", getRuntimeModel(provider)); const target = resolveAiRuntimeTarget(provider, "chat", getRuntimeModel(provider));
switch (provider) { switch (provider) {
case AiProvider.OLLAMA: { case AiProvider.OLLAMA: {
const ollama = createOllamaClient(target); const ollama = createOllamaClient(target);
const result: any = await ollama.list(); const result = await ollama.list() as ModelListResponse;
return (result.models ?? []).map((m: any) => m.model || m.name).filter(Boolean); return (result.models ?? []).map(m => m.model || m.name).filter((name): name is string => !!name);
} }
case AiProvider.GEMINI: { case AiProvider.GEMINI: {
const models: string[] = []; const models: string[] = [];
if (getGeminiApiMode(target) === "openai") { if (getGeminiApiMode(target) === "openai") {
const geminiAi = createGeminiOpenAiClient(target); const geminiAi = createGeminiOpenAiClient(target);
const iterable: any = await geminiAi.models.list(); const iterable = await geminiAi.models.list() as AsyncIterable<NamedModel>;
for await (const model of iterable) models.push(model.name || model.id || String(model)); for await (const model of iterable) models.push(model.name || model.id || String(model));
return models; return models;
} }
const geminiAi = createGoogleGenAiClient(target); const geminiAi = createGoogleGenAiClient(target);
const iterable: any = await geminiAi.models.list(); const iterable = await geminiAi.models.list() as AsyncIterable<NamedModel>;
for await (const model of iterable) { for await (const model of iterable) {
const name = model.name || model.id || String(model); const name = model.name || model.id || String(model);
models.push(String(name).replace(/^models\//, "")); models.push(String(name).replace(/^models\//, ""));
@@ -323,13 +335,14 @@ export async function listProviderModels(provider: AiProvider): Promise<string[]
} }
case AiProvider.MISTRAL: { case AiProvider.MISTRAL: {
const mistralAi = createMistralClient(target); const mistralAi = createMistralClient(target);
const result: any = await mistralAi.models.list(); const result = await mistralAi.models.list() as ModelListResponse | NamedModel[];
return (result.data ?? result.models ?? result ?? []).map((m: any) => m.id || m.name || String(m)).filter(Boolean); 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: { case AiProvider.OPENAI: {
const openAi = createOpenAiClient(target); const openAi = createOpenAiClient(target);
const result: any = await openAi.models.list(); const result = await openAi.models.list() as ModelListResponse;
return (result.data ?? []).map((m: any) => m.id).filter(Boolean); return (result.data ?? []).map(m => m.id).filter((id): id is string => !!id);
} }
} }
} }
+23 -13
View File
@@ -1,5 +1,8 @@
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider"; import {AiProvider} from "../model/ai-provider";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("ai-provider-queue");
export type AiRequestQueueTarget = { export type AiRequestQueueTarget = {
provider: AiProvider; provider: AiProvider;
@@ -7,11 +10,11 @@ export type AiRequestQueueTarget = {
baseUrl?: string; baseUrl?: string;
}; };
type QueueEntry<T> = { type QueueEntry = {
target: AiRequestQueueTarget; target: AiRequestQueueTarget;
queueKey: string; queueKey: string;
run: () => Promise<T>; run: () => Promise<unknown>;
resolve: (value: T | PromiseLike<T>) => void; resolve: (value: unknown) => void;
reject: (reason?: unknown) => void; reject: (reason?: unknown) => void;
onPositionChange: (requestsBefore: number) => Promise<void> | void; onPositionChange: (requestsBefore: number) => Promise<void> | void;
signal?: AbortSignal; signal?: AbortSignal;
@@ -26,21 +29,22 @@ type EnqueueOptions<T> = {
}; };
class AiProviderRequestQueue { class AiProviderRequestQueue {
private readonly waiting = new Map<string, Array<QueueEntry<any>>>(); private readonly waiting = new Map<string, QueueEntry[]>();
private readonly active = new Map<string, number>(); private readonly active = new Map<string, number>();
enqueue<T>(target: AiRequestQueueTarget, options: EnqueueOptions<T>): Promise<T> { enqueue<T>(target: AiRequestQueueTarget, options: EnqueueOptions<T>): Promise<T> {
if (options.signal?.aborted) { if (options.signal?.aborted) {
logger.debug("enqueue.rejected.aborted", {provider: target.provider, model: target.model, baseUrl: target.baseUrl});
return Promise.reject(new Error("Aborted")); return Promise.reject(new Error("Aborted"));
} }
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const queueKey = this.queueKey(target); const queueKey = this.queueKey(target);
const entry: QueueEntry<T> = { const entry: QueueEntry = {
target, target,
queueKey, queueKey,
run: options.run, run: options.run,
resolve, resolve: value => resolve(value as T),
reject, reject,
onPositionChange: options.onPositionChange, onPositionChange: options.onPositionChange,
signal: options.signal, signal: options.signal,
@@ -53,21 +57,23 @@ class AiProviderRequestQueue {
const removed = this.removeWaitingEntry(entry); const removed = this.removeWaitingEntry(entry);
if (!removed) return; if (!removed) return;
logger.debug("entry.cancelled", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queueKey});
reject(new Error("Aborted")); reject(new Error("Aborted"));
this.schedule(target); this.schedule(target);
}; };
options.signal?.addEventListener("abort", entry.abortHandler, {once: true}); options.signal?.addEventListener("abort", entry.abortHandler, {once: true});
this.getOrCreateQueue(queueKey).push(entry); this.getOrCreateQueue(queueKey).push(entry);
logger.debug("enqueue.accepted", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queued: this.getOrCreateQueue(queueKey).length, active: this.activeCount(queueKey)});
this.schedule(target); this.schedule(target);
}); });
} }
private getQueue(queueKey: string): Array<QueueEntry<any>> | undefined { private getQueue(queueKey: string): QueueEntry[] | undefined {
return this.waiting.get(queueKey); return this.waiting.get(queueKey);
} }
private getOrCreateQueue(queueKey: string): Array<QueueEntry<any>> { private getOrCreateQueue(queueKey: string): QueueEntry[] {
let queue = this.waiting.get(queueKey); let queue = this.waiting.get(queueKey);
if (!queue) { if (!queue) {
queue = []; queue = [];
@@ -104,7 +110,7 @@ class AiProviderRequestQueue {
]); ]);
} }
private removeWaitingEntry(entry: QueueEntry<any>): boolean { private removeWaitingEntry(entry: QueueEntry): boolean {
const queue = this.getQueue(entry.queueKey); const queue = this.getQueue(entry.queueKey);
if (!queue) return false; if (!queue) return false;
@@ -132,12 +138,14 @@ class AiProviderRequestQueue {
} }
if (entry.signal?.aborted) { if (entry.signal?.aborted) {
logger.debug("entry.skipped.aborted", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queueKey});
entry.reject(new Error("Aborted")); entry.reject(new Error("Aborted"));
continue; continue;
} }
entry.started = true; entry.started = true;
this.setActiveCount(queueKey, this.activeCount(queueKey) + 1); this.setActiveCount(queueKey, this.activeCount(queueKey) + 1);
logger.debug("entry.started", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queued: queue.length, active: this.activeCount(queueKey)});
void this.runEntry(entry); void this.runEntry(entry);
} }
@@ -147,10 +155,12 @@ class AiProviderRequestQueue {
} }
} }
private async runEntry(entry: QueueEntry<any>): Promise<void> { private async runEntry(entry: QueueEntry): Promise<void> {
try { try {
entry.resolve(await entry.run()); entry.resolve(await entry.run());
logger.debug("entry.done", {provider: entry.target.provider, model: entry.target.model, baseUrl: entry.target.baseUrl});
} catch (e) { } catch (e) {
logger.error("entry.failed", {provider: entry.target.provider, model: entry.target.model, baseUrl: entry.target.baseUrl, error: e});
entry.reject(e); entry.reject(e);
} finally { } finally {
this.setActiveCount(entry.queueKey, this.activeCount(entry.queueKey) - 1); this.setActiveCount(entry.queueKey, this.activeCount(entry.queueKey) - 1);
@@ -168,13 +178,13 @@ class AiProviderRequestQueue {
})).then(results => { })).then(results => {
for (const result of results) { for (const result of results) {
if (result.status === "rejected") { if (result.status === "rejected") {
console.error(result.reason); logger.error("position_update.failed", {provider: target.provider, model: target.model, reason: result.reason});
} }
} }
}).catch(console.error); }).catch(error => logger.error("position_updates.failed", {provider: target.provider, model: target.model, error}));
} }
private deleteQueueIfIdle(queueKey: string, queue: Array<QueueEntry<any>>): void { private deleteQueueIfIdle(queueKey: string, queue: QueueEntry[]): void {
if (!queue.length && this.activeCount(queueKey) <= 0) { if (!queue.length && this.activeCount(queueKey) <= 0) {
this.waiting.delete(queueKey); this.waiting.delete(queueKey);
} }
+12 -10
View File
@@ -198,7 +198,7 @@ async function transcribeGeminiSpeech(audio: AiDownloadedFile, signal?: AbortSig
temperature: 0, temperature: 0,
abortSignal: signal, abortSignal: signal,
}, },
}); }) as unknown as GeminiSpeechResponse;
return { return {
provider: AiProvider.GEMINI, provider: AiProvider.GEMINI,
@@ -240,17 +240,19 @@ async function transcribeOllamaSpeech(audio: AiDownloadedFile, signal?: AbortSig
}; };
} }
function collectGeminiText(response: any): string { type GeminiSpeechResponse = {
if (typeof response?.text === "string") return response.text; text?: string;
candidates?: Array<{content?: {parts?: Array<{text?: string}>}}> ;
};
const candidates = response?.candidates ?? []; function collectGeminiText(response: GeminiSpeechResponse): string {
const candidateText = candidates if (typeof response.text === "string") return response.text;
.flatMap((candidate: any) => candidate?.content?.parts ?? [])
.map((part: any) => part?.text ?? "") const candidateText = (response.candidates ?? [])
.flatMap(candidate => candidate.content?.parts ?? [])
.map(part => part.text ?? "")
.join(""); .join("");
if (candidateText.trim()) return candidateText; if (candidateText.trim()) return candidateText;
return (response?.candidates ?? []) return "";
.map((output: any) => typeof output === "string" ? output : output?.content?.parts?.[0]?.text ?? "")
.join("");
} }
+31 -5
View File
@@ -8,6 +8,7 @@ import {StoredAttachment, StoredAttachmentKind} from "../model/stored-attachment
import {performFFmpeg} from "../util/ffmpeg"; import {performFFmpeg} from "../util/ffmpeg";
import ffmpeg from "fluent-ffmpeg"; import ffmpeg from "fluent-ffmpeg";
import {AsyncSemaphore, KeyedAsyncLock} from "../util/async-lock"; import {AsyncSemaphore, KeyedAsyncLock} from "../util/async-lock";
import {appLogger} from "../logging/logger";
export type AiDownloadedFile = { export type AiDownloadedFile = {
kind: StoredAttachmentKind; kind: StoredAttachmentKind;
@@ -20,6 +21,7 @@ export type AiDownloadedFile = {
const cachePathLocks = new KeyedAsyncLock(); const cachePathLocks = new KeyedAsyncLock();
const ffmpegSemaphore = new AsyncSemaphore(2); const ffmpegSemaphore = new AsyncSemaphore(2);
const logger = appLogger.child("attachments");
function safeFileName(value: string): string { function safeFileName(value: string): string {
return value.replace(/[\\/:*?"<>|\u0000-\u001F]/g, "_").slice(0, 180); return value.replace(/[\\/:*?"<>|\u0000-\u001F]/g, "_").slice(0, 180);
@@ -90,31 +92,48 @@ function cachePathFor(kind: StoredAttachmentKind, fileUniqueId: string | undefin
} }
async function downloadToCache(kind: StoredAttachmentKind, fileId: string, fileName: string, mimeType?: string, fileUniqueId?: string): Promise<StoredAttachment | null> { async function downloadToCache(kind: StoredAttachmentKind, fileId: string, fileName: string, mimeType?: string, fileUniqueId?: string): Promise<StoredAttachment | null> {
const startedAt = Date.now();
logger.debug("download.start", {kind, fileId, fileName, mimeType});
const file = await bot.getFile({file_id: fileId}); const file = await bot.getFile({file_id: fileId});
const finalFileName = fileNameWithExtension(fileName, mimeType, file.file_path); const finalFileName = fileNameWithExtension(fileName, mimeType, file.file_path);
const location = cachePathFor(kind, fileUniqueId, fileId, finalFileName); const location = cachePathFor(kind, fileUniqueId, fileId, finalFileName);
await cachePathLocks.runExclusive(location, async () => { await cachePathLocks.runExclusive(location, async () => {
if (fs.existsSync(location)) return; if (fs.existsSync(location)) {
logger.trace("download.cache_hit", {kind, location});
return;
}
const buffer = await downloadTelegramFile(file.file_path); const buffer = await downloadTelegramFile(file.file_path);
if (!buffer) return; if (!buffer) {
logger.warn("download.empty", {kind, fileId, telegramFilePath: file.file_path});
return;
}
const tempLocation = `${location}.${process.pid}.${Date.now()}.tmp`; const tempLocation = `${location}.${process.pid}.${Date.now()}.tmp`;
fs.mkdirSync(path.dirname(location), {recursive: true}); fs.mkdirSync(path.dirname(location), {recursive: true});
fs.writeFileSync(tempLocation, buffer); fs.writeFileSync(tempLocation, buffer);
fs.renameSync(tempLocation, location); fs.renameSync(tempLocation, location);
logger.debug("download.saved", {kind, location, bytes: buffer.length, duration: logger.duration(startedAt)});
}); });
return {kind, fileId, fileUniqueId, fileName: finalFileName, mimeType, cachePath: location}; return {kind, fileId, fileUniqueId, fileName: finalFileName, mimeType, cachePath: location};
} }
async function convertAudioToWav(input: string, output: string, noVideo = false): Promise<void> { async function convertAudioToWav(input: string, output: string, noVideo = false): Promise<void> {
const startedAt = Date.now();
logger.debug("audio.convert.start", {input, output, noVideo});
await cachePathLocks.runExclusive(output, async () => { await cachePathLocks.runExclusive(output, async () => {
if (fs.existsSync(output)) return; if (fs.existsSync(output)) {
logger.trace("audio.convert.cache_hit", {output});
return;
}
await ffmpegSemaphore.runExclusive(async () => { await ffmpegSemaphore.runExclusive(async () => {
if (fs.existsSync(output)) return; if (fs.existsSync(output)) {
logger.trace("audio.convert.cache_hit", {output});
return;
}
const tempOutput = `${output}.${process.pid}.${Date.now()}.tmp.wav`; const tempOutput = `${output}.${process.pid}.${Date.now()}.tmp.wav`;
try { try {
@@ -125,14 +144,16 @@ async function convertAudioToWav(input: string, output: string, noVideo = false)
.toFormat("wav") .toFormat("wav")
.save(tempOutput) .save(tempOutput)
.on("progress", (progress) => { .on("progress", (progress) => {
console.log("progress", progress); logger.trace("audio.convert.progress", {input, output, progress});
}); });
}); });
fs.renameSync(tempOutput, output); fs.renameSync(tempOutput, output);
logger.debug("audio.convert.done", {input, output, duration: logger.duration(startedAt)});
} catch (e) { } catch (e) {
if (fs.existsSync(tempOutput)) { if (fs.existsSync(tempOutput)) {
fs.rmSync(tempOutput, {force: true}); fs.rmSync(tempOutput, {force: true});
} }
logger.error("audio.convert.failed", {input, output, error: e});
throw e; throw e;
} }
}); });
@@ -140,7 +161,9 @@ async function convertAudioToWav(input: string, output: string, noVideo = false)
} }
export async function cacheMessageAttachments(msg: Message): Promise<StoredAttachment[]> { export async function cacheMessageAttachments(msg: Message): Promise<StoredAttachment[]> {
const startedAt = Date.now();
const result: StoredAttachment[] = []; const result: StoredAttachment[] = [];
logger.debug("message.cache.start", {chatId: msg.chat?.id, messageId: msg.message_id});
try { try {
if (msg.photo?.length) { if (msg.photo?.length) {
@@ -202,10 +225,12 @@ export async function cacheMessageAttachments(msg: Message): Promise<StoredAttac
logError(e); logError(e);
} }
logger.debug("message.cache.done", {chatId: msg.chat?.id, messageId: msg.message_id, attachments: result.length, duration: logger.duration(startedAt)});
return result; return result;
} }
export function attachmentsToDownloadedFiles(attachments: StoredAttachment[]): AiDownloadedFile[] { export function attachmentsToDownloadedFiles(attachments: StoredAttachment[]): AiDownloadedFile[] {
logger.trace("downloaded_files.build", {attachments: attachments.length});
return attachments return attachments
.filter(attachment => fs.existsSync(attachment.cachePath)) .filter(attachment => fs.existsSync(attachment.cachePath))
.map(attachment => ({ .map(attachment => ({
@@ -219,6 +244,7 @@ export function attachmentsToDownloadedFiles(attachments: StoredAttachment[]): A
} }
export function cleanupDownloads(files: AiDownloadedFile[]): void { export function cleanupDownloads(files: AiDownloadedFile[]): void {
logger.trace("downloaded_files.cleanup", {files: files.length});
// Files stay on disk in the message cache; drop in-memory buffers eagerly. // Files stay on disk in the message cache; drop in-memory buffers eagerly.
for (const file of files) { for (const file of files) {
file.buffer = Buffer.alloc(0); file.buffer = Buffer.alloc(0);
+4 -3
View File
@@ -292,7 +292,7 @@ export class TelegramStreamMessage {
} }
if (shouldRemoveKeyboard) await this.removeKeyboard(); if (shouldRemoveKeyboard) await this.removeKeyboard();
this.lastSent = next; this.lastSent = next;
} catch (e: any) { } catch (e: unknown) {
if (shouldRemoveKeyboard && this.isMessageNotModified(e)) { if (shouldRemoveKeyboard && this.isMessageNotModified(e)) {
await this.removeKeyboard(); await this.removeKeyboard();
this.lastSent = next; this.lastSent = next;
@@ -369,8 +369,9 @@ export class TelegramStreamMessage {
if (result && result !== true) this.waitMessage = result; if (result && result !== true) this.waitMessage = result;
this.mediaMode = true; this.mediaMode = true;
this.lastSent = next; this.lastSent = next;
} catch (e: any) { } catch (e: unknown) {
if (!String(e?.message ?? e).includes("message is not modified")) logError(e); const message = e instanceof Error ? e.message : String(e);
if (!message.includes("message is not modified")) logError(e);
} }
} }
+2 -2
View File
@@ -171,7 +171,7 @@ async function synthesizeMistralSpeech(text: string, voice?: string): Promise<Sy
if (target.model) request.model = target.model; if (target.model) request.model = target.model;
if (voice || Environment.MISTRAL_TTS_VOICE_ID) request.voiceId = voice || Environment.MISTRAL_TTS_VOICE_ID; if (voice || Environment.MISTRAL_TTS_VOICE_ID) request.voiceId = voice || Environment.MISTRAL_TTS_VOICE_ID;
const response: any = await mistralAi.audio.speech.complete(request); const response = await mistralAi.audio.speech.complete(request) as unknown as {audioData?: string; audio_data?: string};
const audioData = response?.audioData ?? response?.audio_data; const audioData = response?.audioData ?? response?.audio_data;
if (typeof audioData !== "string" || !audioData.trim()) { if (typeof audioData !== "string" || !audioData.trim()) {
throw new Error(Environment.mistralTtsNoAudioDataText); throw new Error(Environment.mistralTtsNoAudioDataText);
@@ -192,7 +192,7 @@ async function synthesizeMistralSpeech(text: string, voice?: string): Promise<Sy
async function synthesizeGeminiSpeech(text: string, voice?: string): Promise<SynthesizedSpeech> { async function synthesizeGeminiSpeech(text: string, voice?: string): Promise<SynthesizedSpeech> {
const target = resolveAiRuntimeTarget(AiProvider.GEMINI, "textToSpeech"); const target = resolveAiRuntimeTarget(AiProvider.GEMINI, "textToSpeech");
const geminiAi = createGoogleGenAiClient(target); const geminiAi = createGoogleGenAiClient(target);
const response: any = await geminiAi.models.generateContent({ const response = await geminiAi.models.generateContent({
model: target.model, model: target.model,
contents: text, contents: text,
config: { config: {
+9 -1
View File
@@ -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 => ({ return getTools().map(tool => ({
type: "function", type: "function",
name: tool.function.name, name: tool.function.name,
+2 -25
View File
@@ -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 = { export type AiToolParameters = {
type: "object"; type: "object";
@@ -45,7 +22,7 @@ export type AiToolCall = {
function: { function: {
name: string; name: string;
arguments: { arguments: {
[key: string]: any; [key: string]: unknown;
}; };
}; };
}; };
+10 -5
View File
@@ -1,4 +1,7 @@
import axios from "axios"; import axios from "axios";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("brave-search");
import {Environment} from "../../common/environment"; import {Environment} from "../../common/environment";
import {logError} from "../../util/utils"; import {logError} from "../../util/utils";
import {AiTool} from "../tool-types"; import {AiTool} from "../tool-types";
@@ -264,7 +267,8 @@ function normalizeBraveResultFilter(value: unknown): string {
} }
export async function webSearch(args?: Record<string, unknown>) { export async function webSearch(args?: Record<string, unknown>) {
console.log("braveSearch()"); const startedAt = Date.now();
logger.info("start", {args});
try { try {
const query = asNonEmptyString(args?.query); const query = asNonEmptyString(args?.query);
@@ -356,11 +360,12 @@ export async function webSearch(args?: Record<string, unknown>) {
note: "Use returned URLs as sources. Do not invent facts that are not present in the snippets/results.", 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); logError(e);
const status = e?.response?.status; const axiosLike = e as {response?: {status?: unknown; data?: unknown}};
const data = e?.response?.data; const status = axiosLike.response?.status;
const data = axiosLike.response?.data;
return { return {
ok: false, ok: false,
@@ -369,7 +374,7 @@ export async function webSearch(args?: Record<string, unknown>) {
response: data ?? null, response: data ?? null,
}; };
} finally { } finally {
console.log("END: braveSearch()"); logger.debug("done", {duration: logger.duration(startedAt)});
} }
} }
+90
View File
@@ -0,0 +1,90 @@
import {AiTool} from "../tool-types";
import path from "node:path";
import {readFile, writeFile} from "node:fs/promises";
import {NOTES_HEADER, notesDir, notesRootFile} from "../../index";
import {asNonEmptyString} from "./utils";
import fs from "node:fs";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("create-note");
export type CreateNoteResult =
| { success: true; filePath: string }
| { success: false; error: string };
export const createNoteTool = {
type: "function",
function: {
name: "create_note",
description: "Create a new Markdown note with a valid file name, optional title, and Markdown-formatted content.",
parameters: {
type: "object",
properties: {
fileName: {
type: "string",
description: "The valid file name for the note. It must be suitable for use as a file name and must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters. Use a clear, concise name based on the note topic. Include the .md extension if the user provides it or if Markdown files are expected."
},
title: {
type: "string",
description: "The title of the note. Use a concise, human-readable title based on the user's request or the note content."
},
content: {
type: "string",
description: "The full content of the note formatted as valid Markdown. Preserve existing Markdown formatting when provided. If the source content has little or no formatting, add appropriate Markdown structure such as headings, paragraphs, lists, links, code blocks, tables, or emphasis where useful, without changing the meaning."
}
},
required: ["fileName", "content"],
}
}
} satisfies AiTool;
export async function createNote(
args?: Record<string, unknown>
): Promise<CreateNoteResult> {
const startedAt = Date.now();
logger.debug("start", {args});
const fileName = asNonEmptyString(args?.fileName) ?? "";
if (!fileName.trim().length) {
return {success: false, error: "No file name provided"};
}
const title = asNonEmptyString(args?.title) ?? fileName;
const content = asNonEmptyString(args?.content) ?? "";
if (!content.trim().length) {
return {success: false, error: "No content provided"};
}
const newFilePath = path.join(notesDir, fileName.endsWith(".md") ? fileName : fileName + ".md");
const linkMarkdown = `* [${title}](${path.relative(path.dirname(notesRootFile), newFilePath)})`;
try {
if (fs.existsSync(newFilePath)) {
return {success: false, error: "File already exists"};
}
await writeFile(newFilePath, content, "utf-8");
let rootContent: string;
try {
rootContent = await readFile(notesRootFile, "utf-8");
} catch (e) {
rootContent = "";
}
const notesHeaderIndex = rootContent.indexOf(NOTES_HEADER);
if (notesHeaderIndex >= 0) {
rootContent += "\n" + linkMarkdown;
} else {
rootContent = NOTES_HEADER + "\n" + linkMarkdown;
}
await writeFile(notesRootFile, rootContent, "utf-8");
logger.debug("done", {fileName, filePath: newFilePath, duration: logger.duration(startedAt)});
return {success: true, filePath: newFilePath};
} catch (error) {
logger.error("failed", {duration: logger.duration(startedAt), error});
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to process files: ${errorMessage}`};
}
}
+4 -4
View File
@@ -327,8 +327,8 @@ async function assertNoSymlinkInPath(
if (stat.isSymbolicLink()) { if (stat.isSymbolicLink()) {
throw new Error("Symlinks are not allowed in file tool paths."); throw new Error("Symlinks are not allowed in file tool paths.");
} }
} catch (e: any) { } catch (e: unknown) {
if (e?.code === "ENOENT" && options?.allowMissingTail) { if ((e as NodeJS.ErrnoException).code === "ENOENT" && options?.allowMissingTail) {
return; return;
} }
@@ -341,8 +341,8 @@ async function pathExists(absolutePath: string): Promise<boolean> {
try { try {
await fs.promises.lstat(absolutePath); await fs.promises.lstat(absolutePath);
return true; return true;
} catch (e: any) { } catch (e: unknown) {
if (e?.code === "ENOENT") return false; if ((e as NodeJS.ErrnoException).code === "ENOENT") return false;
throw e; throw e;
} }
} }
+318
View File
@@ -0,0 +1,318 @@
import {AiTool} from "../tool-types";
import path from "node:path";
import {readdir, readFile, unlink, writeFile} from "node:fs/promises";
import {notesDir, notesRootFile} from "../../index";
import {asNonEmptyString} from "./utils";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("notes");
export type NoteListItem = {
fileName: string;
filePath: string;
relativePath: string;
title: string;
};
export type ListNotesResult =
| { success: true; notes: NoteListItem[] }
| { success: false; error: string };
export type GetNoteContentResult =
| {
success: true;
fileName: string;
filePath: string;
relativePath: string;
title: string;
content: string;
} | { success: false; error: string };
export const listNotesTool = {
type: "function",
function: {
name: "list_notes",
description: "Display all available Markdown notes from the notes directory.",
parameters: {
type: "object",
properties: {},
required: [],
},
},
} satisfies AiTool;
export const getNoteContentTool = {
type: "function",
function: {
name: "get_note_content",
description: "Get the full Markdown content of a specific note by its file name.",
parameters: {
type: "object",
properties: {
fileName: {
type: "string",
description:
"The file name of the note to read. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
},
},
required: ["fileName"],
},
},
} satisfies AiTool;
export async function listNotes(): Promise<ListNotesResult> {
const startedAt = Date.now();
logger.debug("list.start");
try {
const entries = await readdir(notesDir, {withFileTypes: true});
const markdownFiles = entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((fileName) => fileName.endsWith(".md"));
const notes: NoteListItem[] = await Promise.all(
markdownFiles.map(async (fileName) => {
const filePath = path.join(notesDir, fileName);
const relativePath = path.relative(path.dirname(notesRootFile), filePath);
let content = "";
try {
content = await readFile(filePath, "utf-8");
} catch {
// Ignore content read errors for individual files.
}
return {
fileName,
filePath,
relativePath,
title: extractNoteTitle(fileName, content),
};
}),
);
notes.sort((a, b) => a.title.localeCompare(b.title));
logger.debug("list.done", {notes: notes.length, duration: logger.duration(startedAt)});
return {success: true, notes};
} catch (error) {
logger.error("list.failed", {duration: logger.duration(startedAt), error});
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to list notes: ${errorMessage}`};
}
}
export async function getNoteContent(
args?: Record<string, unknown>,
): Promise<GetNoteContentResult> {
const startedAt = Date.now();
logger.debug("get_content.start", {args});
const fileName = asNonEmptyString(args?.fileName) ?? "";
if (!fileName.trim().length) {
return {success: false, error: "No file name provided"};
}
const noteFilePath = buildSafeNoteFilePath(fileName);
if (!noteFilePath) {
return {success: false, error: "Invalid or unsafe file name provided"};
}
try {
const content = await readFile(noteFilePath, "utf-8");
const normalizedFileName = path.basename(noteFilePath);
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
logger.debug("get_content.done", {fileName: normalizedFileName, relativePath, chars: content.length, duration: logger.duration(startedAt)});
return {
success: true,
fileName: normalizedFileName,
filePath: noteFilePath,
relativePath,
title: extractNoteTitle(normalizedFileName, content),
content,
};
} catch (error) {
logger.error("list.failed", {duration: logger.duration(startedAt), error});
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to read note: ${errorMessage}`};
}
}
function extractNoteTitle(fileName: string, content: string): string {
const headingMatch = content.match(/^#\s+(.+)$/m);
const heading = headingMatch?.[1]?.trim();
if (heading) {
return heading;
}
return path.basename(fileName, ".md");
}
export function buildSafeNoteFilePath(fileName: string): string | null {
const normalizedFileName = fileName.endsWith(".md") ? fileName : `${fileName}.md`;
if (!normalizedFileName.trim().length) {
return null;
}
const unsafeFileNamePattern = /[/\\:*?"<>|\x00-\x1F]/;
if (unsafeFileNamePattern.test(normalizedFileName)) {
return null;
}
const resolvedNotesDir = path.resolve(notesDir);
const resolvedFilePath = path.resolve(notesDir, normalizedFileName);
if (!resolvedFilePath.startsWith(resolvedNotesDir + path.sep)) {
return null;
}
return resolvedFilePath;
}
export type UpdateNoteContentResult =
| { success: true; filePath: string }
| { success: false; error: string };
export type DeleteNoteResult =
| { success: true; filePath: string }
| { success: false; error: string };
export const updateNoteContentTool = {
type: "function",
function: {
name: "update_note_content",
description: "Update the full Markdown content of an existing note by its file name.",
parameters: {
type: "object",
properties: {
fileName: {
type: "string",
description:
"The file name of the note to update. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
},
content: {
type: "string",
description:
"The new full content of the note formatted as valid Markdown. This replaces the previous content completely.",
},
},
required: ["fileName", "content"],
},
},
} satisfies AiTool;
export const deleteNoteTool = {
type: "function",
function: {
name: "delete_note",
description: "Delete an existing Markdown note by its file name and remove its link from the notes root file if present.",
parameters: {
type: "object",
properties: {
fileName: {
type: "string",
description:
"The file name of the note to delete. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
},
},
required: ["fileName"],
},
},
} satisfies AiTool;
export async function updateNoteContent(
args?: Record<string, unknown>,
): Promise<UpdateNoteContentResult> {
const startedAt = Date.now();
logger.debug("update_content.start", {args});
const fileName = asNonEmptyString(args?.fileName) ?? "";
if (!fileName.trim().length) {
return {success: false, error: "No file name provided"};
}
const content = asNonEmptyString(args?.content) ?? "";
if (!content.trim().length) {
return {success: false, error: "No content provided"};
}
const noteFilePath = buildSafeNoteFilePath(fileName);
if (!noteFilePath) {
return {success: false, error: "Invalid or unsafe file name provided"};
}
try {
await readFile(noteFilePath, "utf-8");
await writeFile(noteFilePath, content, "utf-8");
logger.debug("update_content.done", {fileName, filePath: noteFilePath, chars: content.length, duration: logger.duration(startedAt)});
return {success: true, filePath: noteFilePath};
} catch (error) {
logger.error("list.failed", {duration: logger.duration(startedAt), error});
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to update note: ${errorMessage}`};
}
}
export async function deleteNote(
args?: Record<string, unknown>,
): Promise<DeleteNoteResult> {
const startedAt = Date.now();
logger.debug("delete.start", {args});
const fileName = asNonEmptyString(args?.fileName) ?? "";
if (!fileName.trim().length) {
return {success: false, error: "No file name provided"};
}
const noteFilePath = buildSafeNoteFilePath(fileName);
if (!noteFilePath) {
return {success: false, error: "Invalid or unsafe file name provided"};
}
try {
await unlink(noteFilePath);
await removeNoteLinkFromRoot(noteFilePath);
logger.debug("delete.done", {fileName, filePath: noteFilePath, duration: logger.duration(startedAt)});
return {success: true, filePath: noteFilePath};
} catch (error) {
logger.error("list.failed", {duration: logger.duration(startedAt), error});
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to delete note: ${errorMessage}`};
}
}
async function removeNoteLinkFromRoot(noteFilePath: string): Promise<void> {
let rootContent: string;
try {
rootContent = await readFile(notesRootFile, "utf-8");
} catch {
return;
}
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
const normalizedRelativePath = relativePath.replaceAll("\\", "\\\\");
const escapedRelativePath = escapeRegExp(normalizedRelativePath);
const linkLinePattern = new RegExp(
`^\\s*[-*]\\s+\\[[^\\]]+]\\(${escapedRelativePath}\\)\\s*$\\n?`,
"gm",
);
const updatedRootContent = rootContent.replace(linkLinePattern, "");
if (updatedRootContent !== rootContent) {
await writeFile(notesRootFile, updatedRootContent.trimEnd() + "\n", "utf-8");
}
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+9 -3
View File
@@ -1,5 +1,8 @@
import {AiTool} from "../tool-types"; import {AiTool} from "../tool-types";
import axios from "axios"; import axios from "axios";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("market-rates");
export const getMarketRatesTool = { export const getMarketRatesTool = {
type: "function", type: "function",
@@ -58,12 +61,15 @@ export const marketRatesToolPrompt = [
"- When the user asks for all rates, group fiat currencies separately from crypto and gold.", "- When the user asks for all rates, group fiat currencies separately from crypto and gold.",
].join("\n"); ].join("\n");
export async function getMarketRates(): Promise<any | undefined> { export async function getMarketRates(): Promise<unknown | undefined> {
const startedAt = Date.now();
try { try {
logger.info("start");
const response = await axios.get("https://apid.r00t.top/api/v2/currency/rates"); const response = await axios.get("https://apid.r00t.top/api/v2/currency/rates");
logger.debug("done", {duration: logger.duration(startedAt), status: response.status});
return response.data; return response.data;
} catch (e: any) { } catch (e: unknown) {
console.error("GET_MARKET_RATES", e); logger.error("failed", {duration: logger.duration(startedAt), error: e});
return undefined; return undefined;
} }
} }
+17 -13
View File
@@ -5,6 +5,9 @@ import path from "node:path";
import {AiTool} from "../tool-types"; import {AiTool} from "../tool-types";
import {Environment} from "../../common/environment"; import {Environment} from "../../common/environment";
import {randomUUID} from "node:crypto"; import {randomUUID} from "node:crypto";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("python-interpreter");
export const PYTHON_INTERPRETER_TOOL_NAME = "python_interpreter"; export const PYTHON_INTERPRETER_TOOL_NAME = "python_interpreter";
@@ -203,17 +206,17 @@ export async function runPythonInterpreter(
}; };
} }
console.time("python.syntax"); const syntaxStartedAt = Date.now();
const syntax = await validatePythonSyntax(args.code, options); const syntax = await validatePythonSyntax(args.code, options);
console.timeEnd("python.syntax"); logger.debug("syntax.done", {duration: logger.duration(syntaxStartedAt), ok: syntax.ok});
if (!syntax.ok) { if (!syntax.ok) {
return syntax; return syntax;
} }
console.time("python.execution"); const executionStartedAt = Date.now();
const result = await executePythonCode(args, options); const result = await executePythonCode(args, options);
console.timeEnd("python.execution"); logger.debug("execution.done", {duration: logger.duration(executionStartedAt), ok: result.ok, phase: result.phase});
return result; return result;
} }
@@ -293,7 +296,8 @@ async function executePythonCode(
args: PythonInterpreterArgs, args: PythonInterpreterArgs,
options: PythonInterpreterOptions = {}, options: PythonInterpreterOptions = {},
): Promise<PythonToolResult> { ): Promise<PythonToolResult> {
console.log("EXECUTE_PYTHON_CODE", "ARGS: ", JSON.stringify(args), "; OPTIONS: ", JSON.stringify(options)); const startedAt = Date.now();
logger.info("execute.start", {args, options});
const pythonBinary = const pythonBinary =
options.pythonBinary ?? process.env.PYTHON_INTERPRETER_BINARY ?? "C:\\Users\\meloda\\Desktop\\AI_BOT\\.venv\\Scripts\\python.exe"; options.pythonBinary ?? process.env.PYTHON_INTERPRETER_BINARY ?? "C:\\Users\\meloda\\Desktop\\AI_BOT\\.venv\\Scripts\\python.exe";
@@ -329,7 +333,7 @@ async function executePythonCode(
mode: 0o600, mode: 0o600,
}); });
console.log("EXECUTE_PYTHON_CODE", "SCRIPT FILE WRITTEN", new Date()); logger.debug("script.written", {tempDir, userScriptPath, runnerPath, duration: logger.duration(startedAt)});
const result = await runProcess({ const result = await runProcess({
command: pythonBinary, command: pythonBinary,
@@ -346,10 +350,10 @@ async function executePythonCode(
}, },
}); });
console.log("EXECUTE_PYTHON_CODE", "RESULT ACHIEVED", new Date()); logger.debug("process.done", {duration: logger.duration(startedAt), exitCode: result.exitCode, timedOut: result.timedOut, outputTruncated: result.outputTruncated});
if (result.timedOut) { if (result.timedOut) {
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR TIMED OUT", new Date()); logger.warn("process.timeout", {duration: logger.duration(startedAt)});
return { return {
ok: false, ok: false,
phase: "execution", phase: "execution",
@@ -365,7 +369,7 @@ async function executePythonCode(
} }
if (result.outputTruncated) { if (result.outputTruncated) {
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR TRUNCATED", new Date()); logger.warn("process.output_truncated", {duration: logger.duration(startedAt), stdoutChars: result.stdout.length, stderrChars: result.stderr.length});
return { return {
ok: false, ok: false,
@@ -382,7 +386,7 @@ async function executePythonCode(
} }
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR EXIT CODE", new Date(), "\n", JSON.stringify(result, null, 2)); logger.warn("process.non_zero_exit", {duration: logger.duration(startedAt), result});
return { return {
ok: false, ok: false,
@@ -398,7 +402,7 @@ async function executePythonCode(
}; };
} }
console.log("EXECUTE_PYTHON_CODE", "RESULT NORMAL", new Date()); logger.debug("process.ok", {duration: logger.duration(startedAt)});
const { const {
artifacts, artifacts,
@@ -420,7 +424,7 @@ async function executePythonCode(
skippedArtifacts, skippedArtifacts,
}; };
} catch (error) { } catch (error) {
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR", new Date()); logger.error("execute.failed", {duration: logger.duration(startedAt), error});
return { return {
ok: false, ok: false,
phase: "internal", phase: "internal",
@@ -692,7 +696,7 @@ function parsePythonInterpreterArgs(
return { return {
code, code,
stdin, stdin: typeof stdin === "string" ? stdin : undefined,
timeoutMs: timeoutMs === undefined ? undefined : Number(timeoutMs), timeoutMs: timeoutMs === undefined ? undefined : Number(timeoutMs),
}; };
} }
+37
View File
@@ -25,11 +25,31 @@ import {
updateFile, updateFile,
updateFileTool updateFileTool
} from "./file-system"; } from "./file-system";
import {createNote, createNoteTool} from "./create-note";
import {
deleteNote,
deleteNoteTool,
getNoteContent,
getNoteContentTool,
listNotes,
listNotesTool,
updateNoteContent,
updateNoteContentTool
} from "./list-notes";
import {getNoteFile, getNoteFileTool} from "./send-note-file";
import {searchNotes, searchNotesTool} from "./search-notes";
export const getTools = () => { export const getTools = () => {
const tools: AiTool[] = [ const tools: AiTool[] = [
getCurrentDateTimeTool, getCurrentDateTimeTool,
getMarketRatesTool, getMarketRatesTool,
createNoteTool,
listNotesTool,
getNoteContentTool,
updateNoteContentTool,
deleteNoteTool,
getNoteFileTool,
searchNotesTool
]; ];
if (Environment.ENABLE_PYTHON_INTERPRETER) { if (Environment.ENABLE_PYTHON_INTERPRETER) {
@@ -61,13 +81,30 @@ export const getTools = () => {
); );
} }
return tools; return tools;
// return [
// createNoteTool,
// listNotesTool,
// getNoteContentTool,
// updateNoteContentTool,
// deleteNoteTool,
// getNoteFileTool,
// searchNotesTool
// ];
}; };
export const getToolHandlers = () => { export const getToolHandlers = () => {
let handlers: Record<string, ToolHandler> = { let handlers: Record<string, ToolHandler> = {
get_datetime: getCurrentDateTime, get_datetime: getCurrentDateTime,
get_market_rates: getMarketRates, get_market_rates: getMarketRates,
create_note: createNote,
list_notes: listNotes,
get_note_content: getNoteContent,
update_note_content: updateNoteContent,
delete_note: deleteNote,
get_note_file: getNoteFile,
search_notes: searchNotes
}; };
if (Environment.ENABLE_PYTHON_INTERPRETER) { if (Environment.ENABLE_PYTHON_INTERPRETER) {
+10 -2
View File
@@ -1,6 +1,9 @@
import {getToolHandlers} from "./registry"; import {getToolHandlers} from "./registry";
import {normalizeToolArguments} from "./utils"; import {normalizeToolArguments} from "./utils";
import {PYTHON_INTERPRETER_TOOL_NAME, PythonInterpreterInputFile, runPythonInterpreter} from "./python-interpretator"; import {PYTHON_INTERPRETER_TOOL_NAME, PythonInterpreterInputFile, runPythonInterpreter} from "./python-interpretator";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("runtime");
export type ToolRuntimeContext = { export type ToolRuntimeContext = {
pythonInputFiles?: PythonInterpreterInputFile[]; pythonInputFiles?: PythonInterpreterInputFile[];
@@ -16,7 +19,9 @@ export async function executeToolCall(
args?: unknown, args?: unknown,
context: ToolRuntimeContext = {}, context: ToolRuntimeContext = {},
): Promise<string> { ): Promise<string> {
const startedAt = Date.now();
const handler = getToolHandlers()[name]; const handler = getToolHandlers()[name];
logger.info("execute.start", {name, args});
if (!handler) { if (!handler) {
return stringifyToolResult({ return stringifyToolResult({
@@ -35,14 +40,17 @@ export async function executeToolCall(
}); });
const s = stringifyToolResult(result); const s = stringifyToolResult(result);
console.log("PYTHON_INTERPRETER_STRING_RESULT", s); logger.debug("execute.done", {name, chars: s.length, duration: logger.duration(startedAt)});
return s; return s;
} }
const result = await handler(normalizeToolArguments(args)); const result = await handler(normalizeToolArguments(args));
return stringifyToolResult(result); const s = stringifyToolResult(result);
logger.debug("execute.done", {name, chars: s.length, duration: logger.duration(startedAt)});
return s;
} catch (error) { } catch (error) {
logger.error("execute.failed", {name, duration: logger.duration(startedAt), error});
return stringifyToolResult({ return stringifyToolResult({
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
}); });
+394
View File
@@ -0,0 +1,394 @@
import {AiTool} from "../tool-types";
import path from "node:path";
import {readdir, readFile} from "node:fs/promises";
import {notesDir, notesRootFile} from "../../index";
import {asNonEmptyString} from "./utils";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("search-notes");
export type SearchNoteMatchedField = "file_name" | "title" | "content";
export type SearchNoteItem = {
fileName: string;
filePath: string;
relativePath: string;
title: string;
score: number;
matchedFields: SearchNoteMatchedField[];
snippet?: string;
};
export type SearchNotesResult =
| { success: true; results: SearchNoteItem[] }
| { success: false; error: string };
export const searchNotesTool = {
type: "function",
function: {
name: "search_notes",
description:
"Search Markdown notes by file name, note title, and full note content. Supports fuzzy matching. Use this when the user refers to a note by title, topic, partial title, approximate name, keyword, or something written inside the note. Returns success=true and results[], where each result contains fileName, title, score, matchedFields, relativePath, and optional snippet. Later note tools should use results[0].fileName unless multiple results are ambiguous.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description:
"Search query for finding notes by file name, title, topic, keywords, or content. Can be partial, approximate, or contain typos. Use a short clean phrase, not the full user sentence.",
},
limit: {
type: "integer",
description:
"Maximum number of search results to return. Defaults to 3. Maximum is 10.",
minimum: 1,
maximum: 10,
default: 3,
},
},
required: ["query"],
},
},
} satisfies AiTool;
export async function searchNotes(
args?: Record<string, unknown>,
): Promise<SearchNotesResult> {
const startedAt = Date.now();
logger.debug("start", {args});
const query = asNonEmptyString(args?.query) ?? "";
if (!query.trim().length) {
return {success: false, error: "No query provided"};
}
const limit = parseSearchLimit(args?.limit);
try {
const entries = await readdir(notesDir, {withFileTypes: true});
const markdownFiles = entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((fileName) => fileName.endsWith(".md"));
const notes = await Promise.all(
markdownFiles.map(async (fileName) => {
const filePath = path.join(notesDir, fileName);
const relativePath = path.relative(path.dirname(notesRootFile), filePath);
let content = "";
try {
content = await readFile(filePath, "utf-8");
} catch {
// Ignore content read errors for individual files.
}
const title = extractNoteTitle(fileName, content);
const fileNameWithoutExtension = path.basename(fileName, ".md");
const fileNameScore = calculateFuzzyScore(query, fileNameWithoutExtension);
const titleScore = calculateFuzzyScore(query, title);
const contentScore = calculateContentScore(query, content);
const matchedFields: SearchNoteMatchedField[] = [];
if (fileNameScore > 0) {
matchedFields.push("file_name");
}
if (titleScore > 0) {
matchedFields.push("title");
}
if (contentScore > 0) {
matchedFields.push("content");
}
const score = Math.max(
fileNameScore,
titleScore,
contentScore,
);
return {
fileName,
filePath,
relativePath,
title,
score,
matchedFields,
snippet:
contentScore > 0
? buildContentSnippet(query, content)
: undefined,
};
}),
);
const results = notes
.filter((note) => note.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
logger.debug("done", {query, limit, results: results.length, duration: logger.duration(startedAt)});
return {success: true, results};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to search notes: ${errorMessage}`};
}
}
function parseSearchLimit(value: unknown): number {
const parsed =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseInt(value, 10)
: 3;
if (!Number.isFinite(parsed)) {
return 3;
}
return Math.max(1, Math.min(10, Math.floor(parsed)));
}
function extractNoteTitle(fileName: string, content: string): string {
const headingMatch = content.match(/^#\s+(.+)$/m);
const heading = headingMatch?.[1]?.trim();
if (heading) {
return heading;
}
return path.basename(fileName, ".md");
}
function calculateFuzzyScore(query: string, value: string): number {
const normalizedQuery = normalizeSearchText(query);
const normalizedValue = normalizeSearchText(value);
if (!normalizedQuery.length || !normalizedValue.length) {
return 0;
}
if (normalizedValue === normalizedQuery) {
return 100;
}
if (normalizedValue.startsWith(normalizedQuery)) {
return 90;
}
if (normalizedValue.includes(normalizedQuery)) {
return 85;
}
const queryWords = normalizedQuery.split(" ").filter(Boolean);
const valueWords = normalizedValue.split(" ").filter(Boolean);
const wordMatchScore = calculateWordMatchScore(queryWords, valueWords);
const subsequenceScore = isSubsequence(normalizedQuery, normalizedValue) ? 55 : 0;
const distanceScore = calculateLevenshteinScore(normalizedQuery, normalizedValue);
return Math.max(wordMatchScore, subsequenceScore, distanceScore);
}
function calculateContentScore(query: string, content: string): number {
const normalizedQuery = normalizeSearchText(query);
const normalizedContent = normalizeSearchText(content);
if (!normalizedQuery.length || !normalizedContent.length) {
return 0;
}
if (normalizedContent.includes(normalizedQuery)) {
return 70;
}
const queryWords = normalizedQuery.split(" ").filter(Boolean);
const contentWords = new Set(normalizedContent.split(" ").filter(Boolean));
if (!queryWords.length) {
return 0;
}
let matchedWords = 0;
for (const queryWord of queryWords) {
if (contentWords.has(queryWord)) {
matchedWords++;
continue;
}
const hasPartialMatch = [...contentWords].some((contentWord) => {
if (contentWord.includes(queryWord) || queryWord.includes(contentWord)) {
return true;
}
if (queryWord.length < 4 || contentWord.length < 4) {
return false;
}
const distance = levenshteinDistance(queryWord, contentWord);
const maxLength = Math.max(queryWord.length, contentWord.length);
const similarity = 1 - distance / maxLength;
return similarity >= 0.75;
});
if (hasPartialMatch) {
matchedWords += 0.75;
}
}
const matchRatio = matchedWords / queryWords.length;
if (matchRatio <= 0) {
return 0;
}
return Math.round(matchRatio * 60);
}
function normalizeSearchText(value: string): string {
return value
.toLowerCase()
.trim()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ё/g, "е")
.replace(/[^a-zа-я0-9\s-]/gi, " ")
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ");
}
function calculateWordMatchScore(queryWords: string[], valueWords: string[]): number {
if (!queryWords.length || !valueWords.length) {
return 0;
}
let matchedWords = 0;
for (const queryWord of queryWords) {
const bestWordScore = Math.max(
...valueWords.map((valueWord) => {
if (valueWord === queryWord) {
return 1;
}
if (valueWord.startsWith(queryWord) || valueWord.includes(queryWord)) {
return 0.85;
}
const distance = levenshteinDistance(queryWord, valueWord);
const maxLength = Math.max(queryWord.length, valueWord.length);
const similarity = 1 - distance / maxLength;
return similarity >= 0.7 ? similarity : 0;
}),
);
if (bestWordScore > 0) {
matchedWords += bestWordScore;
}
}
const ratio = matchedWords / queryWords.length;
return Math.round(ratio * 75);
}
function calculateLevenshteinScore(query: string, value: string): number {
const distance = levenshteinDistance(query, value);
const maxLength = Math.max(query.length, value.length);
if (maxLength === 0) {
return 0;
}
const similarity = 1 - distance / maxLength;
if (similarity < 0.45) {
return 0;
}
return Math.round(similarity * 65);
}
function isSubsequence(query: string, value: string): boolean {
let queryIndex = 0;
for (const valueChar of value) {
if (valueChar === query[queryIndex]) {
queryIndex++;
}
if (queryIndex === query.length) {
return true;
}
}
return false;
}
function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = Array.from({length: a.length + 1}, () =>
Array.from({length: b.length + 1}, () => 0),
);
for (let i = 0; i <= a.length; i++) {
matrix[i][0] = i;
}
for (let j = 0; j <= b.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost,
);
}
}
return matrix[a.length][b.length];
}
function buildContentSnippet(query: string, content: string): string | undefined {
const normalizedQuery = query.trim().toLowerCase();
const normalizedContent = content.toLowerCase();
let matchIndex = normalizedContent.indexOf(normalizedQuery);
if (matchIndex < 0) {
const queryWords = normalizeSearchText(query)
.split(" ")
.filter((word) => word.length >= 3);
for (const word of queryWords) {
matchIndex = normalizedContent.indexOf(word);
if (matchIndex >= 0) {
break;
}
}
}
if (matchIndex < 0) {
return undefined;
}
const snippetRadius = 120;
const start = Math.max(0, matchIndex - snippetRadius);
const end = Math.min(content.length, matchIndex + normalizedQuery.length + snippetRadius);
const prefix = start > 0 ? "..." : "";
const suffix = end < content.length ? "..." : "";
return `${prefix}${content.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
}
+113
View File
@@ -0,0 +1,113 @@
import {AiTool} from "../tool-types";
import path from "node:path";
import {readFile, stat} from "node:fs/promises";
import {notesRootFile} from "../../index";
import {asNonEmptyString} from "./utils";
import {buildSafeNoteFilePath} from "./list-notes";
import z from "zod";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("get-note-file");
export type NoteFileAttachment = {
type: "local_file";
fileName: string;
// filePath: string;
relativePath: string;
mimeType: "text/markdown";
sizeBytes: number;
};
export type GetNoteFileResult =
| {
success: true;
attachment: NoteFileAttachment;
} | { success: false; error: string };
export const NoteFileAttachmentSchema = z.object({
type: z.literal("local_file"),
fileName: z.string(),
// filePath: z.string(),
relativePath: z.string(),
mimeType: z.literal("text/markdown"),
sizeBytes: z.number(),
});
export const GetNoteFileResultSchema = z.discriminatedUnion("success", [
z.object({
success: z.literal(true),
attachment: NoteFileAttachmentSchema,
}),
z.object({
success: z.literal(false),
error: z.string(),
}),
]);
export const getNoteFileTool = {
type: "function",
function: {
name: "get_note_file",
description:
"Prepare a Markdown note file to be sent to the user as a .md attachment. Returns a local file descriptor that the host application should use to upload or send the file.",
parameters: {
type: "object",
properties: {
fileName: {
type: "string",
description:
"The file name of the note to send. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
},
},
required: ["fileName"],
},
},
} satisfies AiTool;
export async function getNoteFile(
args?: Record<string, unknown>,
): Promise<GetNoteFileResult> {
logger.debug("start", {args});
const fileName = asNonEmptyString(args?.fileName) ?? "";
if (!fileName.trim().length) {
return {success: false, error: "No file name provided"};
}
const noteFilePath = buildSafeNoteFilePath(fileName);
if (!noteFilePath) {
return {success: false, error: "Invalid or unsafe file name provided"};
}
try {
// Проверяем, что файл существует и действительно читается.
await readFile(noteFilePath, "utf-8");
const fileStat = await stat(noteFilePath);
if (!fileStat.isFile()) {
return {success: false, error: "Note path is not a file"};
}
const normalizedFileName = path.basename(noteFilePath);
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
const result: GetNoteFileResult = {
success: true,
attachment: {
type: "local_file",
fileName: normalizedFileName,
// filePath: noteFilePath,
relativePath,
mimeType: "text/markdown",
sizeBytes: fileStat.size,
},
};
logger.debug("done", {fileName: result.attachment.fileName, relativePath: result.attachment.relativePath, sizeBytes: result.attachment.sizeBytes});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {success: false, error: `Failed to prepare note file: ${errorMessage}`};
}
}
+32 -4
View File
@@ -1,4 +1,8 @@
import {Ollama} from "ollama"; import {Ollama} from "ollama";
import {z} from "zod";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("utils");
export function asNonEmptyString(value: unknown): string | undefined { export function asNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 return typeof value === "string" && value.trim().length > 0
@@ -78,14 +82,15 @@ export async function unloadAllOllamaModels(ollama: Ollama, exceptFor?: string[]
); );
await Promise.all(unloadPromises); await Promise.all(unloadPromises);
console.log("All models have been requested to unload" + exceptFor?.length ? ` except for [${exceptFor?.join(", ")}].` : "."); logger.info("ollama.unload_all.done", {count: modelsToUnload.length, exceptFor});
} catch (error) { } catch (error) {
console.error("Error unloading models:", error); logger.error("ollama.unload_all.failed", {exceptFor, error});
} }
} }
export async function loadOllamaModel(model: string, ollama: Ollama, contextLength: number): Promise<boolean> { export async function loadOllamaModel(model: string, ollama: Ollama, contextLength: number): Promise<boolean> {
try { try {
logger.info("ollama.load.start", {model, contextLength});
await ollama.generate({ await ollama.generate({
model: model, model: model,
stream: false, stream: false,
@@ -94,9 +99,32 @@ export async function loadOllamaModel(model: string, ollama: Ollama, contextLeng
num_ctx: contextLength num_ctx: contextLength
} }
}); });
logger.info("ollama.load.done", {model, contextLength});
return true; return true;
} catch (e: any) { } catch (e: unknown) {
console.error("Error loading Ollama model:", model); logger.error("ollama.load.failed", {model, contextLength, error: e});
return false; return false;
} }
} }
export type ToolPlanStep = {
t: string;
h: string;
from: string;
};
export type RouterPlan = {
s: ToolPlanStep[];
m: string;
};
export const ToolPlanStepSchema = z.object({
t: z.string(),
h: z.string(),
from: z.string(),
});
export const RouterPlanSchema = z.object({
s: z.array(ToolPlanStepSchema),
m: z.string()
});
+11 -6
View File
@@ -1,4 +1,7 @@
import axios from "axios"; import axios from "axios";
import {toolsLogger} from "./tool-logger";
const logger = toolsLogger.child("weather");
import {Environment} from "../../common/environment"; import {Environment} from "../../common/environment";
import {logError} from "../../util/utils"; import {logError} from "../../util/utils";
import {AiTool} from "../tool-types"; import {AiTool} from "../tool-types";
@@ -42,8 +45,9 @@ export const weatherToolPrompt = [
"If the city is missing or unclear, ask the user to specify it.", "If the city is missing or unclear, ask the user to specify it.",
].join("\n"); ].join("\n");
export async function getWeather(args?: Record<string, unknown>): Promise<any | null> { export async function getWeather(args?: Record<string, unknown>): Promise<Record<string, unknown> | null> {
console.log("getWeather()"); const startedAt = Date.now();
logger.info("start", {args});
try { try {
const city = asNonEmptyString(args?.city); const city = asNonEmptyString(args?.city);
const lang = asNonEmptyString(args?.lang); const lang = asNonEmptyString(args?.lang);
@@ -61,7 +65,7 @@ export async function getWeather(args?: Record<string, unknown>): Promise<any |
appid: apiKey, appid: apiKey,
}, },
})).data[0]; })).data[0];
console.log("GEOCODE_RESPONSE", geocodeResponse); logger.debug("geocode.done", {city, country: geocodeResponse?.country, hasResult: !!geocodeResponse, geocodeResponse});
if (!geocodeResponse) { if (!geocodeResponse) {
return { return {
ok: false, ok: false,
@@ -83,7 +87,7 @@ export async function getWeather(args?: Record<string, unknown>): Promise<any |
...(lang ? {lang} : {}), ...(lang ? {lang} : {}),
}, },
})).data; })).data;
console.log("RESPONSE: getWeather(lang=" + lang + "): ", response); logger.debug("weather_api.done", {city, country: geocodeResponse.country, lang, units: "metric", hasResponse: !!response});
const main = response.main; const main = response.main;
const sys = response.sys; const sys = response.sys;
@@ -137,10 +141,11 @@ export async function getWeather(args?: Record<string, unknown>): Promise<any |
windSpeed: wind.speed, windSpeed: wind.speed,
}, },
}; };
} catch (e: any) { } catch (e: unknown) {
logger.error("failed", {duration: logger.duration(startedAt), error: e});
logError(e); logError(e);
return null; return null;
} finally { } finally {
console.log("END: getWeather()"); logger.debug("done", {duration: logger.duration(startedAt)});
} }
} }
+156
View File
@@ -0,0 +1,156 @@
// Gemini provider runner extracted from unified-ai-runner.ts.
import {getGeminiTools} from "./tool-mappers";
import {TelegramStreamMessage} from "./telegram-stream-message";
import {ToolRuntimeContext} from "./tools/runtime";
import {GeminiMessage} from "./gemini-chat-message";
import {createGoogleGenAiClient} from "./ai-runtime-target";
import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
import {AsyncIterableStream, GeminiFunctionCallLike, GeminiResponseLike, MAX_TOOL_ROUNDS, RuntimeConfigSnapshot, ToolCallData, ToolExecutionMemory, executeToolBatch, roundStatus, safeJsonParseObject, GeminiGenerationRequest} from "./unified-ai-runner.shared";
function collectGeminiResponseText(response: GeminiResponseLike & { text?: string }): string {
if (typeof response.text === "string") return response.text;
return (response.candidates ?? [])
.flatMap(candidate => 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<void> {
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<GeminiResponseLike & {
text?: string
}>;
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;
}
+137
View File
@@ -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 "../logging/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<void> {
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<typeof mistralAi.chat.complete>[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<typeof mistralAi.chat.stream>[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;
}
+404
View File
@@ -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 "../logging/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<void> {
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<typeof setInterval> | 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;
}
+424
View File
@@ -0,0 +1,424 @@
// 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 "../logging/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<void> {
// 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[],
tools: getOpenAIResponsesToolsWithImage(config) as ResponseCreateParamsNonStreaming["tools"],
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,
tools: getOpenAIResponsesToolsWithImage(config) as ResponseCreateParamsStreaming["tools"],
};
const response = await openAi.responses.create(request, {signal}) as unknown as AsyncIterableStream<ResponseStreamEvent>;
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<void> {
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<void> {
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<OpenAiChatCompletionStreamChunkLike>;
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;
}
File diff suppressed because it is too large Load Diff
+225
View File
@@ -0,0 +1,225 @@
import {Tool} from "ollama";
import {AiRuntimeTarget, createOllamaClient} from "./ai-runtime-target";
import {aiLog, aiLogDuration, aiLogProviderTarget} from "../logging/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<ToolRankerSelection> {
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>): 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.`;
+130 -1946
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -96,8 +96,8 @@ export class AiCancel extends CallbackCommand {
{method: "editMessageText", chatId: message.chat.id, chatType: message.chat.type} {method: "editMessageText", chatId: message.chat.id, chatType: message.chat.type}
); );
if (result && result !== true) { if (result) {
await MessageStore.put({...result, text: cancelledText} as Message); await MessageStore.put({...(result as object), text: cancelledText} as Message);
} else { } else {
await MessageStore.put({ await MessageStore.put({
chatId: message.chat.id, chatId: message.chat.id,
+6 -4
View File
@@ -7,7 +7,7 @@ import {StoredMessage} from "../model/stored-message";
import {cutPrefixes, logError} from "../util/utils"; import {cutPrefixes, logError} from "../util/utils";
import {runUnifiedAi} from "../ai/unified-ai-runner"; import {runUnifiedAi} from "../ai/unified-ai-runner";
import {AI_REGENERATE_CALLBACK, parseAiRegenerateCallbackData} from "../ai/regenerate-callback"; import {AI_REGENERATE_CALLBACK, parseAiRegenerateCallbackData} from "../ai/regenerate-callback";
import {isAiProviderConfigured, resolveEffectiveAiProviderForUser} from "../common/user-ai-settings"; import {resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
export class AiRegenerate extends CallbackCommand { export class AiRegenerate extends CallbackCommand {
@@ -28,9 +28,11 @@ export class AiRegenerate extends CallbackCommand {
const sourceFromId = source.stored?.fromId ?? source.message.from?.id; const sourceFromId = source.stored?.fromId ?? source.message.from?.id;
if (!sourceFromId || (sourceFromId !== query.from.id && query.from.id !== Environment.CREATOR_ID)) return; if (!sourceFromId || (sourceFromId !== query.from.id && query.from.id !== Environment.CREATOR_ID)) return;
const provider = isAiProviderConfigured(parsed.provider) const provider =
? parsed.provider // isAiProviderConfigured(parsed.provider)
: await resolveEffectiveAiProviderForUser(source.message.from?.id ?? query.from.id); // ? parsed.provider
// :
await resolveEffectiveAiProviderForUser(source.message.from?.id ?? query.from.id);
const text = cutPrefixes(source.stored ?? source.message) ?? ""; const text = cutPrefixes(source.stored ?? source.message) ?? "";
runUnifiedAi({ runUnifiedAi({
+8 -6
View File
@@ -21,8 +21,9 @@ export class Ae extends Command {
try { try {
let result = this.executeEvaluation(match); let result = this.executeEvaluation(match);
await oldSendMessage(msg, result).catch(async () => await errorPlaceholder(msg)); await oldSendMessage(msg, result).catch(async () => await errorPlaceholder(msg));
} catch (e: any) { } catch (e: unknown) {
const text = e.message.toString(); const error = e instanceof Error ? e : new Error(String(e));
const text = error.message.toString();
if (text.includes("is not defined")) { if (text.includes("is not defined")) {
await oldSendMessage(msg, Environment.variableNotDefinedText).catch(logError); await oldSendMessage(msg, Environment.variableNotDefinedText).catch(logError);
@@ -30,7 +31,7 @@ export class Ae extends Command {
} }
logError(`${text} logError(`${text}
* Stacktrace: ${e.stack}`); * Stacktrace: ${error.stack}`);
await oldSendMessage(msg, text).catch(logError); await oldSendMessage(msg, text).catch(logError);
} }
@@ -43,15 +44,16 @@ export class Ae extends Command {
e = ((typeof e == "string") ? e : JSON.stringify(e)); e = ((typeof e == "string") ? e : JSON.stringify(e));
return e; return e;
} catch (e: any) { } catch (e: unknown) {
const text = e.message.toString(); const error = e instanceof Error ? e : new Error(String(e));
const text = error.message.toString();
if (text.includes("is not defined")) { if (text.includes("is not defined")) {
return Environment.evaluationVariableNotDefinedText; return Environment.evaluationVariableNotDefinedText;
} }
logError(`${text} logError(`${text}
* Stacktrace: ${e.stack}`); * Stacktrace: ${error.stack}`);
return text; return text;
} }
+4 -1
View File
@@ -3,6 +3,9 @@ import {Message} from "typescript-telegram-bot-api";
import {logError, oldReplyToMessage, randomValue} from "../util/utils"; import {logError, oldReplyToMessage, randomValue} from "../util/utils";
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer"; import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("command:choice");
export class Choice extends Command { export class Choice extends Command {
command = "choice"; command = "choice";
@@ -12,7 +15,7 @@ export class Choice extends Command {
description = Environment.commandDescriptions.choice; description = Environment.commandDescriptions.choice;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match); logger.debug("execute", {chatId: msg.chat?.id, messageId: msg.message_id, match});
const payload = match?.[3] || ""; const payload = match?.[3] || "";
+1 -1
View File
@@ -62,7 +62,7 @@ export class Distort extends Command {
}), }),
{method: "sendPhoto", chatId, chatType: msg.chat.type} {method: "sendPhoto", chatId, chatType: msg.chat.type}
); );
} catch (e: any) { } catch (e: unknown) {
await oldReplyToMessage( await oldReplyToMessage(
msg, Environment.getDistortFailedText(e) msg, Environment.getDistortFailedText(e)
).catch(logError); ).catch(logError);
+9 -11
View File
@@ -1,15 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class GeminiGetModel extends Command { export class GeminiGetModel extends ProviderGetModelCommand {
title = Environment.commandTitles.geminiGetModel; constructor() {
description = Environment.commandDescriptions.geminiGetModel; super({
provider: AiProvider.GEMINI,
async execute(msg: Message): Promise<void> { title: Environment.commandTitles.geminiGetModel,
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.GEMINI)}).catch(logError); description: Environment.commandDescriptions.geminiGetModel,
});
} }
} }
+9 -24
View File
@@ -1,28 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderListModelsCommand} from "./provider-model-command";
export class GeminiListModels extends Command { export class GeminiListModels extends ProviderListModelsCommand {
title = Environment.commandTitles.geminiListModels; constructor() {
description = Environment.commandDescriptions.geminiListModels; super({
provider: AiProvider.GEMINI,
requirements = Requirements.Build(Requirement.BOT_CREATOR); title: Environment.commandTitles.geminiListModels,
description: Environment.commandDescriptions.geminiListModels,
async execute(msg: Message): Promise<void> { });
try {
const models = (await listProviderModels(AiProvider.GEMINI)).sort((a, b) => a.localeCompare(b));
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({message: msg, text, parse_mode: "HTML"});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
}
} }
} }
+9 -25
View File
@@ -1,29 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class GeminiSetModel extends Command { export class GeminiSetModel extends ProviderSetModelCommand {
argsMode = "required" as const; constructor() {
super({
title = Environment.commandTitles.geminiSetModel; provider: AiProvider.GEMINI,
description = Environment.commandDescriptions.geminiSetModel; title: Environment.commandTitles.geminiSetModel,
description: Environment.commandDescriptions.geminiSetModel,
requirements = Requirements.Build(Requirement.BOT_CREATOR); });
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]?.trim();
if (newModel) setRuntimeModel(AiProvider.GEMINI, newModel);
const model = getRuntimeModel(AiProvider.GEMINI);
const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.GEMINI))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text}).catch(logError);
} }
} }
+9 -11
View File
@@ -1,15 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class MistralGetModel extends Command { export class MistralGetModel extends ProviderGetModelCommand {
title = Environment.commandTitles.mistralGetModel; constructor() {
description = Environment.commandDescriptions.mistralGetModel; super({
provider: AiProvider.MISTRAL,
async execute(msg: Message): Promise<void> { title: Environment.commandTitles.mistralGetModel,
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.MISTRAL)}).catch(logError); description: Environment.commandDescriptions.mistralGetModel,
});
} }
} }
+9 -24
View File
@@ -1,28 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderListModelsCommand} from "./provider-model-command";
export class MistralListModels extends Command { export class MistralListModels extends ProviderListModelsCommand {
title = Environment.commandTitles.mistralListModels; constructor() {
description = Environment.commandDescriptions.mistralListModels; super({
provider: AiProvider.MISTRAL,
requirements = Requirements.Build(Requirement.BOT_CREATOR); title: Environment.commandTitles.mistralListModels,
description: Environment.commandDescriptions.mistralListModels,
async execute(msg: Message): Promise<void> { });
try {
const models = (await listProviderModels(AiProvider.MISTRAL)).sort((a, b) => a.localeCompare(b));
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({message: msg, text, parse_mode: "HTML"});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
}
} }
} }
+9 -25
View File
@@ -1,29 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class MistralSetModel extends Command { export class MistralSetModel extends ProviderSetModelCommand {
argsMode = "required" as const; constructor() {
super({
title = Environment.commandTitles.mistralSetModel; provider: AiProvider.MISTRAL,
description = Environment.commandDescriptions.mistralSetModel; title: Environment.commandTitles.mistralSetModel,
description: Environment.commandDescriptions.mistralSetModel,
requirements = Requirements.Build(Requirement.BOT_CREATOR); });
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]?.trim();
if (newModel) setRuntimeModel(AiProvider.MISTRAL, newModel);
const model = getRuntimeModel(AiProvider.MISTRAL);
const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.MISTRAL))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text}).catch(logError);
} }
} }
+9 -11
View File
@@ -1,15 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class OllamaGetModel extends Command { export class OllamaGetModel extends ProviderGetModelCommand {
title = Environment.commandTitles.ollamaGetModel; constructor() {
description = Environment.commandDescriptions.ollamaGetModel; super({
provider: AiProvider.OLLAMA,
async execute(msg: Message): Promise<void> { title: Environment.commandTitles.ollamaGetModel,
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.OLLAMA)}).catch(logError); description: Environment.commandDescriptions.ollamaGetModel,
});
} }
} }
+8 -30
View File
@@ -1,35 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider"; import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime"; import {ProviderListModelsCommand} from "./provider-model-command";
import {createOllamaClient, resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
export class OllamaListModels extends Command { export class OllamaListModels extends ProviderListModelsCommand {
title = Environment.commandTitles.ollamaListModels; constructor() {
description = Environment.commandDescriptions.ollamaListModels; super({
provider: AiProvider.OLLAMA,
requirements = Requirements.Build(Requirement.BOT_CREATOR); title: Environment.commandTitles.ollamaListModels,
description: Environment.commandDescriptions.ollamaListModels,
async execute(msg: Message): Promise<void> { });
try {
const target = resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat");
const models = (await listProviderModels(AiProvider.OLLAMA)).sort((a, b) => a.localeCompare(b));
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const loadedModels = ((await createOllamaClient(target).ps())?.models ?? [])
.map(model => model.model || model.name)
.filter((model): model is string => !!model);
const text =
Environment.getLoadedModelsText(loadedModels) + "\n\n" +
Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({message: msg, text, parse_mode: "HTML"});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
}
} }
} }
+9 -25
View File
@@ -1,29 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class OllamaSetModel extends Command { export class OllamaSetModel extends ProviderSetModelCommand {
argsMode = "required" as const; constructor() {
super({
title = Environment.commandTitles.ollamaSetModel; provider: AiProvider.OLLAMA,
description = Environment.commandDescriptions.ollamaSetModel; title: Environment.commandTitles.ollamaSetModel,
description: Environment.commandDescriptions.ollamaSetModel,
requirements = Requirements.Build(Requirement.BOT_CREATOR); });
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]?.trim();
if (newModel) setRuntimeModel(AiProvider.OLLAMA, newModel);
const model = getRuntimeModel(AiProvider.OLLAMA);
const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.OLLAMA))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text}).catch(logError);
} }
} }
+9 -11
View File
@@ -1,15 +1,13 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderGetModelCommand} from "./provider-model-command";
export class OpenAIGetModel extends Command { export class OpenAIGetModel extends ProviderGetModelCommand {
title = Environment.commandTitles.openAiGetModel; constructor() {
description = Environment.commandDescriptions.openAiGetModel; super({
provider: AiProvider.OPENAI,
async execute(msg: Message): Promise<void> { title: Environment.commandTitles.openAiGetModel,
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.OPENAI)}).catch(logError); description: Environment.commandDescriptions.openAiGetModel,
});
} }
} }
+9 -24
View File
@@ -1,28 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderListModelsCommand} from "./provider-model-command";
export class OpenAIListModels extends Command { export class OpenAIListModels extends ProviderListModelsCommand {
title = Environment.commandTitles.openAiListModels; constructor() {
description = Environment.commandDescriptions.openAiListModels; super({
provider: AiProvider.OPENAI,
requirements = Requirements.Build(Requirement.BOT_CREATOR); title: Environment.commandTitles.openAiListModels,
description: Environment.commandDescriptions.openAiListModels,
async execute(msg: Message): Promise<void> { });
try {
const models = (await listProviderModels(AiProvider.OPENAI)).sort((a, b) => a.localeCompare(b));
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({message: msg, text, parse_mode: "HTML"});
} catch (e) {
logError(e);
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
}
} }
} }
+9 -25
View File
@@ -1,29 +1,13 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {ProviderSetModelCommand} from "./provider-model-command";
export class OpenAISetModel extends Command { export class OpenAISetModel extends ProviderSetModelCommand {
argsMode = "required" as const; constructor() {
super({
title = Environment.commandTitles.openAiSetModel; provider: AiProvider.OPENAI,
description = Environment.commandDescriptions.openAiSetModel; title: Environment.commandTitles.openAiSetModel,
description: Environment.commandDescriptions.openAiSetModel,
requirements = Requirements.Build(Requirement.BOT_CREATOR); });
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]?.trim();
if (newModel) setRuntimeModel(AiProvider.OPENAI, newModel);
const model = getRuntimeModel(AiProvider.OPENAI);
const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.OPENAI))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text}).catch(logError);
} }
} }
+1 -1
View File
@@ -69,7 +69,7 @@ export class Qr extends Command {
}), }),
{method: "sendPhoto", chatId, chatType: msg.chat.type} {method: "sendPhoto", chatId, chatType: msg.chat.type}
); );
} catch (e: any) { } catch (e: unknown) {
await replyToMessage({ await replyToMessage({
message: msg, message: msg,
text: Environment.getQrCodeFailedText(e) text: Environment.getQrCodeFailedText(e)
+11 -5
View File
@@ -21,6 +21,9 @@ import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {AsyncSemaphore} from "../util/async-lock"; import {AsyncSemaphore} from "../util/async-lock";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {getLruMapValue, setLruMapValue} from "../util/lru-map"; import {getLruMapValue, setLruMapValue} from "../util/lru-map";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("command:quote");
try { try {
GlobalFonts.registerFromPath("./assets/Inter_18pt-Thin.ttf", "InterThin"); GlobalFonts.registerFromPath("./assets/Inter_18pt-Thin.ttf", "InterThin");
@@ -54,6 +57,8 @@ export class Quote extends Command {
if (!reply) return; if (!reply) return;
try { try {
const startedAt = Date.now();
logger.debug("execute.start", {chatId, messageId: msg.message_id, replyMessageId: reply.message_id});
const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim(); const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim();
if (quoteRaw.length === 0) { if (quoteRaw.length === 0) {
await replyToMessage({message: msg, text: Environment.quoteMissingTextText}).catch(logError); await replyToMessage({message: msg, text: Environment.quoteMissingTextText}).catch(logError);
@@ -74,7 +79,8 @@ export class Quote extends Command {
}, },
}), }),
{method: "sendPhoto", chatId, chatType: msg.chat.type} {method: "sendPhoto", chatId, chatType: msg.chat.type}
).catch(logError); );
logger.debug("execute.done", {chatId, messageId: msg.message_id, bytes: png.length, duration: logger.duration(startedAt)});
} catch (e) { } catch (e) {
logError(e); logError(e);
await replyToMessage({message: msg, text: Environment.quoteBuildFailedText}).catch(logError); await replyToMessage({message: msg, text: Environment.quoteBuildFailedText}).catch(logError);
@@ -146,14 +152,14 @@ async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | nul
}); });
if (!stickerSet || stickerSet.length === 0) { if (!stickerSet || stickerSet.length === 0) {
console.warn(`Custom emoji ${customEmojiId} not found`); logger.warn("custom_emoji.not_found", {customEmojiId});
return null; return null;
} }
const sticker = stickerSet[0]; const sticker = stickerSet[0];
if (sticker.is_animated || sticker.is_video) { if (sticker.is_animated || sticker.is_video) {
console.warn(`Animated/video custom emoji ${customEmojiId} not supported`); logger.warn("custom_emoji.unsupported", {customEmojiId});
return loadEmoji(sticker.emoji); return loadEmoji(sticker.emoji);
} }
@@ -171,7 +177,7 @@ async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | nul
setLruMapValue(customEmojiCache, customEmojiId, img, CUSTOM_EMOJI_CACHE_MAX_ENTRIES); setLruMapValue(customEmojiCache, customEmojiId, img, CUSTOM_EMOJI_CACHE_MAX_ENTRIES);
return img; return img;
} catch (e) { } catch (e) {
console.warn(`Failed to load custom emoji ${customEmojiId}:`, e); logger.warn("custom_emoji.load_failed", {customEmojiId, error: e});
return null; return null;
} }
} }
@@ -521,7 +527,7 @@ async function drawLine(ctx: SKRSContext2D, line: Segment[], x: number, baseline
ctx.drawImage(<Image>img, cx, y, emojiSize, emojiSize); ctx.drawImage(<Image>img, cx, y, emojiSize, emojiSize);
} }
} catch (e) { } catch (e) {
console.warn("Failed to draw custom emoji:", e); logger.warn("custom_emoji.draw_failed", {error: e});
try { try {
const img = await loadEmoji("😥"); const img = await loadEmoji("😥");
+39 -3
View File
@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {parse as parseDotEnv} from "dotenv"; import {parse as parseDotEnv} from "dotenv";
import {z} from "zod"; import {z} from "zod";
import {appLogger} from "../logging/logger";
import {saveData} from "../db/database"; import {saveData} from "../db/database";
import {Answers} from "../model/answers"; import {Answers} from "../model/answers";
@@ -168,6 +169,7 @@ const RuntimeEnvSchema = z.object({
), ),
SYSTEM_PROMPT: optionalStringSchema, SYSTEM_PROMPT: optionalStringSchema,
RANKER_TOOL_PROMPT: optionalStringSchema,
USE_NAMES_IN_PROMPT: booleanWithDefaultSchema(false), USE_NAMES_IN_PROMPT: booleanWithDefaultSchema(false),
USE_SYSTEM_PROMPT: booleanWithDefaultSchema(true), USE_SYSTEM_PROMPT: booleanWithDefaultSchema(true),
@@ -234,7 +236,9 @@ export class Environment {
private static lastEnvMtimeMs: number | undefined; private static lastEnvMtimeMs: number | undefined;
private static lastSystemPromptMtimeMs: number | undefined; private static lastSystemPromptMtimeMs: number | undefined;
private static lastRankerToolPromptMtimeMs: number | undefined;
private static envSystemPrompt: string | undefined; private static envSystemPrompt: string | undefined;
private static envRankerToolPrompt: string | undefined;
static BOT_TOKEN: string = ""; static BOT_TOKEN: string = "";
static TEST_ENVIRONMENT: boolean = false; static TEST_ENVIRONMENT: boolean = false;
@@ -273,6 +277,7 @@ export class Environment {
static DEFAULT_AI_PROVIDER: AiProvider = AiProvider.OLLAMA; static DEFAULT_AI_PROVIDER: AiProvider = AiProvider.OLLAMA;
static SYSTEM_PROMPT?: string; static SYSTEM_PROMPT?: string;
static RANKER_TOOL_PROMPT?: string;
static USE_NAMES_IN_PROMPT: boolean = false; static USE_NAMES_IN_PROMPT: boolean = false;
static USE_SYSTEM_PROMPT: boolean = true; static USE_SYSTEM_PROMPT: boolean = true;
static SEND_TIME_TOOK: boolean = false; static SEND_TIME_TOOK: boolean = false;
@@ -1632,8 +1637,8 @@ export class Environment {
private static getFileMtimeMs(filePath: string): number | undefined { private static getFileMtimeMs(filePath: string): number | undefined {
try { try {
return fs.statSync(filePath).mtimeMs; return fs.statSync(filePath).mtimeMs;
} catch (e: any) { } catch (e: unknown) {
if (e?.code === "ENOENT") { if ((e as NodeJS.ErrnoException).code === "ENOENT") {
return undefined; return undefined;
} }
@@ -1665,6 +1670,10 @@ export class Environment {
return path.join(Environment.DATA_PATH, "SYSTEM_PROMPT.md"); return path.join(Environment.DATA_PATH, "SYSTEM_PROMPT.md");
} }
private static getRankerToolPromptPath(): string {
return path.join(Environment.DATA_PATH, "TOOL_RANKER_PROMPT.md");
}
private static readSystemPrompt(): string | undefined { private static readSystemPrompt(): string | undefined {
const promptPath = Environment.getSystemPromptPath(); const promptPath = Environment.getSystemPromptPath();
@@ -1676,10 +1685,25 @@ export class Environment {
return prompt.length > 0 ? prompt : undefined; return prompt.length > 0 ? prompt : undefined;
} }
private static readRankerToolPromptPath(): string | undefined {
const promptPath = Environment.getRankerToolPromptPath();
if (!fs.existsSync(promptPath)) {
return undefined;
}
const prompt = fs.readFileSync(promptPath, "utf8").trim();
return prompt.length > 0 ? prompt : undefined;
}
private static refreshSystemPrompt(): void { private static refreshSystemPrompt(): void {
Environment.SYSTEM_PROMPT = Environment.readSystemPrompt() ?? Environment.envSystemPrompt; Environment.SYSTEM_PROMPT = Environment.readSystemPrompt() ?? Environment.envSystemPrompt;
} }
private static refreshRankerToolPrompt(): void {
Environment.RANKER_TOOL_PROMPT = Environment.readRankerToolPromptPath() ?? Environment.envRankerToolPrompt;
}
private static applyStartupEnv(env: StartupEnv): void { private static applyStartupEnv(env: StartupEnv): void {
Environment.BOT_TOKEN = env.BOT_TOKEN; Environment.BOT_TOKEN = env.BOT_TOKEN;
Environment.TEST_ENVIRONMENT = env.TEST_ENVIRONMENT; Environment.TEST_ENVIRONMENT = env.TEST_ENVIRONMENT;
@@ -1722,7 +1746,9 @@ export class Environment {
Environment.DEFAULT_AI_PROVIDER = env.DEFAULT_AI_PROVIDER; Environment.DEFAULT_AI_PROVIDER = env.DEFAULT_AI_PROVIDER;
Environment.envSystemPrompt = env.SYSTEM_PROMPT; Environment.envSystemPrompt = env.SYSTEM_PROMPT;
Environment.envRankerToolPrompt = env.RANKER_TOOL_PROMPT;
Environment.SYSTEM_PROMPT = env.SYSTEM_PROMPT; Environment.SYSTEM_PROMPT = env.SYSTEM_PROMPT;
Environment.RANKER_TOOL_PROMPT = env.RANKER_TOOL_PROMPT;
Environment.USE_NAMES_IN_PROMPT = env.USE_NAMES_IN_PROMPT; Environment.USE_NAMES_IN_PROMPT = env.USE_NAMES_IN_PROMPT;
Environment.USE_SYSTEM_PROMPT = env.USE_SYSTEM_PROMPT; Environment.USE_SYSTEM_PROMPT = env.USE_SYSTEM_PROMPT;
Environment.SEND_TIME_TOOK = env.SEND_TIME_TOOK ?? false; Environment.SEND_TIME_TOOK = env.SEND_TIME_TOOK ?? false;
@@ -1783,18 +1809,22 @@ export class Environment {
Environment.applyRuntimeEnv(runtimeEnv); Environment.applyRuntimeEnv(runtimeEnv);
Environment.refreshSystemPrompt(); Environment.refreshSystemPrompt();
Environment.refreshRankerToolPrompt();
Environment.lastEnvMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH); Environment.lastEnvMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH);
Environment.lastSystemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath()); Environment.lastSystemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath());
Environment.lastRankerToolPromptMtimeMs = Environment.getFileMtimeMs(Environment.getRankerToolPromptPath());
} }
static reloadRuntimeConfigIfChanged(): void { static reloadRuntimeConfigIfChanged(): void {
try { try {
const envMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH); const envMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH);
const systemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath()); const systemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath());
const rankerToolPromptMtimeMs = Environment.getFileMtimeMs(Environment.getRankerToolPromptPath());
const envChanged = envMtimeMs !== Environment.lastEnvMtimeMs; const envChanged = envMtimeMs !== Environment.lastEnvMtimeMs;
const systemPromptChanged = systemPromptMtimeMs !== Environment.lastSystemPromptMtimeMs; const systemPromptChanged = systemPromptMtimeMs !== Environment.lastSystemPromptMtimeMs;
const rankerToolPromptChanged = rankerToolPromptMtimeMs !== Environment.lastRankerToolPromptMtimeMs;
Localization.reloadIfChanged(); Localization.reloadIfChanged();
@@ -1808,6 +1838,7 @@ export class Environment {
Environment.applyRuntimeEnv(runtimeEnv); Environment.applyRuntimeEnv(runtimeEnv);
Environment.refreshSystemPrompt(); Environment.refreshSystemPrompt();
Environment.refreshRankerToolPrompt();
Environment.lastEnvMtimeMs = envMtimeMs; Environment.lastEnvMtimeMs = envMtimeMs;
} }
@@ -1815,8 +1846,13 @@ export class Environment {
Environment.refreshSystemPrompt(); Environment.refreshSystemPrompt();
Environment.lastSystemPromptMtimeMs = systemPromptMtimeMs; Environment.lastSystemPromptMtimeMs = systemPromptMtimeMs;
} }
if (rankerToolPromptChanged) {
Environment.refreshRankerToolPrompt();
Environment.lastRankerToolPromptMtimeMs = rankerToolPromptMtimeMs;
}
} catch (e) { } catch (e) {
console.error("Failed to reload runtime environment config", e); appLogger.child("environment").error("runtime_reload.failed", {error: e});
} }
} }
+8 -4
View File
@@ -1,6 +1,9 @@
import {AsyncLocalStorage} from "node:async_hooks"; import {AsyncLocalStorage} from "node:async_hooks";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("localization");
export const DEFAULT_LOCALE = "en"; export const DEFAULT_LOCALE = "en";
export const DEFAULT_LANGUAGE_CHOICE = "default"; export const DEFAULT_LANGUAGE_CHOICE = "default";
@@ -23,8 +26,8 @@ function normalizeLanguageCode(value: string | undefined | null): string | undef
function readMtimeMs(filePath: string): number | undefined { function readMtimeMs(filePath: string): number | undefined {
try { try {
return fs.statSync(filePath).mtimeMs; return fs.statSync(filePath).mtimeMs;
} catch (e: any) { } catch (e: unknown) {
if (e?.code === "ENOENT") return undefined; if ((e as NodeJS.ErrnoException).code === "ENOENT") return undefined;
throw e; throw e;
} }
} }
@@ -235,7 +238,7 @@ export class Localization {
try { try {
bundles.set(locale, JSON.parse(fs.readFileSync(filePath, "utf8")) as LocalizationBundle); bundles.set(locale, JSON.parse(fs.readFileSync(filePath, "utf8")) as LocalizationBundle);
} catch (e) { } catch (e) {
console.error(`Failed to load localization file ${filePath}`, e); logger.error("file_load.failed", {filePath, locale, error: e});
const previous = Localization.bundles.get(locale); const previous = Localization.bundles.get(locale);
if (previous) bundles.set(locale, previous); if (previous) bundles.set(locale, previous);
} }
@@ -244,8 +247,9 @@ export class Localization {
Localization.bundles = bundles; Localization.bundles = bundles;
Localization.fileMtimeMs = mtimes; Localization.fileMtimeMs = mtimes;
Localization.fileSignature = signature; Localization.fileSignature = signature;
logger.debug("reload.done", {force, locales: [...bundles.keys()]});
} catch (e) { } catch (e) {
console.error("Failed to reload localization files", e); logger.error("reload.failed", {error: e});
} }
} }
} }
+6 -5
View File
@@ -4,12 +4,13 @@ import {StoredMessage} from "../model/stored-message";
import {and, eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import {inArray} from "drizzle-orm/sql/expressions/conditions"; import {inArray} from "drizzle-orm/sql/expressions/conditions";
import {Dao} from "../base/dao"; import {Dao} from "../base/dao";
import {appLogger} from "../logging/logger";
import {buildExcludedSet} from "../util/utils"; import {buildExcludedSet} from "../util/utils";
import {StoredAttachment} from "../model/stored-attachment"; import {StoredAttachment} from "../model/stored-attachment";
export class MessageDao extends Dao<StoredMessage> { export class MessageDao extends Dao<StoredMessage> {
private tag: string = "MessageDao"; private readonly logger = appLogger.child("dao:messages");
override async getAll(): Promise<StoredMessage[]> { override async getAll(): Promise<StoredMessage[]> {
await DatabaseManager.ready; await DatabaseManager.ready;
@@ -19,7 +20,7 @@ export class MessageDao extends Dao<StoredMessage> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: getAll()`, `took ${diff}ms; size: ${messages.length}`); this.logger.trace("get_all", {dao: "messages", duration: `${diff}ms`, size: messages.length});
return this.mapFrom(messages); return this.mapFrom(messages);
} }
@@ -40,7 +41,7 @@ export class MessageDao extends Dao<StoredMessage> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: getById(${params.chatId}, ${params.id})`, `took ${diff}ms; size: ${messages.length}`); this.logger.trace("get_by_id", {dao: "messages", chatId: params.chatId, id: params.id, duration: `${diff}ms`, size: messages.length});
const m = messages[0]; const m = messages[0];
if (!m) return null; if (!m) return null;
@@ -63,7 +64,7 @@ export class MessageDao extends Dao<StoredMessage> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: getByIds(${params.chatId}, ${params.ids})`, `took ${diff}ms; size: ${messages.length}`); this.logger.trace("get_by_ids", {dao: "messages", chatId: params.chatId, ids: params.ids, duration: `${diff}ms`, size: messages.length});
return this.mapFrom(messages); return this.mapFrom(messages);
} }
@@ -83,7 +84,7 @@ export class MessageDao extends Dao<StoredMessage> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: insert(size: ${values.length})`, `took ${diff}ms'; inserted: ${r.rowsAffected}`); this.logger.debug("insert", {dao: "messages", duration: `${diff}ms`, size: values.length, rowsAffected: r.rowsAffected});
return true; return true;
} }
+6 -5
View File
@@ -1,5 +1,6 @@
import {StoredUser} from "../model/stored-user"; import {StoredUser} from "../model/stored-user";
import {Dao} from "../base/dao"; import {Dao} from "../base/dao";
import {appLogger} from "../logging/logger";
import {DatabaseManager} from "./database-manager"; import {DatabaseManager} from "./database-manager";
import {UserInsert, usersTable} from "./schema"; import {UserInsert, usersTable} from "./schema";
import {eq} from "drizzle-orm"; import {eq} from "drizzle-orm";
@@ -9,7 +10,7 @@ import {boolToInt, buildExcludedSet} from "../util/utils";
export class UserDao extends Dao<StoredUser> { export class UserDao extends Dao<StoredUser> {
private tag: string = "UserDao"; private readonly logger = appLogger.child("dao:users");
override async getAll(): Promise<StoredUser[]> { override async getAll(): Promise<StoredUser[]> {
await DatabaseManager.ready; await DatabaseManager.ready;
@@ -19,7 +20,7 @@ export class UserDao extends Dao<StoredUser> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: getAll()`, `took ${diff}ms; size: ${users.length}`); this.logger.trace("get_all", {dao: "users", duration: `${diff}ms`, size: users.length});
return this.mapFrom(users); return this.mapFrom(users);
} }
@@ -37,7 +38,7 @@ export class UserDao extends Dao<StoredUser> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: getById(${params.id})`, `took ${diff}ms; size: ${users.length}`); this.logger.trace("get_by_id", {dao: "users", id: params.id, duration: `${diff}ms`, size: users.length});
const u = users[0]; const u = users[0];
if (!u) return null; if (!u) return null;
@@ -57,7 +58,7 @@ export class UserDao extends Dao<StoredUser> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: getByIds(${params.ids})`, `took ${diff}ms; size: ${users.length}`); this.logger.trace("get_by_ids", {dao: "users", ids: params.ids, duration: `${diff}ms`, size: users.length});
return this.mapFrom(users); return this.mapFrom(users);
} }
@@ -78,7 +79,7 @@ export class UserDao extends Dao<StoredUser> {
const now = Date.now(); const now = Date.now();
const diff = now - then; const diff = now - then;
console.log(`${this.tag}: insert(size: ${rows.length})`, `took ${diff}ms; inserted: ${r.rowsAffected}`); this.logger.debug("insert", {dao: "users", duration: `${diff}ms`, size: rows.length, rowsAffected: r.rowsAffected});
return true; return true;
} }
+30 -12
View File
@@ -1,4 +1,5 @@
import "dotenv/config"; import "dotenv/config";
import {appLogger} from "./logging/logger";
import {Environment} from "./common/environment"; import {Environment} from "./common/environment";
import {BotCommand, TelegramBot, User} from "typescript-telegram-bot-api"; import {BotCommand, TelegramBot, User} from "typescript-telegram-bot-api";
import {Command} from "./base/command"; import {Command} from "./base/command";
@@ -189,13 +190,20 @@ export const videoDir = path.join(cacheDir, "video");
export const videoNotesDir = path.join(cacheDir, "video-note"); export const videoNotesDir = path.join(cacheDir, "video-note");
export const videoTempDir = path.join(videoDir, "temp"); export const videoTempDir = path.join(videoDir, "temp");
export const NOTES_HEADER = "## Notes\n";
export const notesDir = path.join(Environment.DATA_PATH, "notes");
export const notesRootFile = path.join(notesDir, "index.md");
const logger = appLogger.child("main");
let isShuttingDown = false; let isShuttingDown = false;
async function shutdown(signal: NodeJS.Signals) { async function shutdown(signal: NodeJS.Signals) {
if (isShuttingDown) return; if (isShuttingDown) return;
isShuttingDown = true; isShuttingDown = true;
console.log(`Received ${signal}. Stopping bot polling...`); logger.warn("shutdown.signal", {signal});
try { try {
await bot.stopPolling(); await bot.stopPolling();
@@ -209,21 +217,33 @@ async function shutdown(signal: NodeJS.Signals) {
async function main() { async function main() {
const start = Date.now(); const start = Date.now();
console.log( logger.info("startup.config", {
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` + testEnvironment: Environment.TEST_ENVIRONMENT,
`DATA_PATH: ${Environment.DATA_PATH}\n` + isDocker: Environment.IS_DOCKER,
`MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\n` + dataPath: Environment.DATA_PATH,
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}\n` + dbPath: Environment.DB_PATH,
`DEFAULT_AI_PROVIDER: ${Environment.DEFAULT_AI_PROVIDER}` });
);
const dirsToCheck = [cacheDir, photoDir, photoGenDir, documentDir, audioDir, videoDir, videoNotesDir, videoTempDir]; const dirsToCheck = [cacheDir, photoDir, photoGenDir, documentDir, audioDir, videoDir, videoNotesDir, videoTempDir, notesDir];
dirsToCheck.forEach(dir => { dirsToCheck.forEach(dir => {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, {recursive: true}); fs.mkdirSync(dir, {recursive: true});
logger.debug("startup.dir_created", {dir});
} }
}); });
const notesRootFilePath = path.join(notesDir, "index.md");
if (!fs.existsSync(notesRootFilePath)) {
fs.writeFileSync(notesRootFilePath, "\n" + NOTES_HEADER);
logger.debug("startup.notes_root_created", {notesRootFilePath});
}
if (!(fs.readFileSync(notesRootFilePath).toString().includes(NOTES_HEADER))) {
fs.appendFileSync(notesRootFilePath, "\n" + NOTES_HEADER);
logger.debug("startup.notes_header_added", {notesRootFilePath});
}
// TODO: 13/05/2026, Danil Nikolaev: maybe add clean cache option (or just save summarizations)
// const now = new Date(); // const now = new Date();
// const midnight = new Date(); // const midnight = new Date();
@@ -231,12 +251,10 @@ async function main() {
// midnight.setDate(now.getDate() + 1); // midnight.setDate(now.getDate() + 1);
// const diff = midnight.getTime() - now.getTime(); // const diff = midnight.getTime() - now.getTime();
// console.log("Clearing up cache will be started in " + diff + "ms");
// clearUpFolderFromOldFiles(cacheDir); // clearUpFolderFromOldFiles(cacheDir);
// delay(diff).then(() => { // delay(diff).then(() => {
// setInterval(() => { // setInterval(() => {
// console.log("Started clearing up cache");
// clearUpFolderFromOldFiles(cacheDir); // clearUpFolderFromOldFiles(cacheDir);
// }, 1000 * 60 * 60 * 24); // }, 1000 * 60 * 60 * 24);
// }); // });
@@ -264,7 +282,7 @@ async function main() {
const end = Date.now(); const end = Date.now();
const diff = Math.abs(end - start); const diff = Math.abs(end - start);
console.log(`Bot started in ${diff}ms!`); logger.success("startup.ready", {duration: `${diff}ms`, commands: cmds.length, botId: botUser.id, botUsername: botUser.username});
} catch (error) { } catch (error) {
logError(error); logError(error);
} }
+1 -2
View File
@@ -1,7 +1,6 @@
export type OllamaRequest = { export type OllamaRequest = {
uuid: string; uuid: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any stream: unknown;
stream: any;
done: boolean; done: boolean;
fromId: number; fromId: number;
chatId: number; chatId: number;
+10 -2
View File
@@ -1,6 +1,9 @@
import {logError} from "./utils"; import {logError} from "./utils";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {appLogger} from "../logging/logger";
const logger = appLogger.child("files");
export function clearUpFolderFromOldFiles(folder: string, recursive = true) { export function clearUpFolderFromOldFiles(folder: string, recursive = true) {
fs.readdir(folder, (err, files) => { fs.readdir(folder, (err, files) => {
@@ -33,11 +36,16 @@ export function clearUpFolderFromOldFiles(folder: string, recursive = true) {
} }
}); });
console.log("filenamesToDelete", filenamesToDelete); logger.debug("cleanup.candidates", {folder, recursive, count: filenamesToDelete.length, filenamesToDelete});
if (filenamesToDelete.length) { if (filenamesToDelete.length) {
filenamesToDelete.forEach((filename) => { filenamesToDelete.forEach((filename) => {
fs.rm(filename, (e) => { fs.rm(filename, (e) => {
if (e) logError(e); if (e) {
logger.error("cleanup.delete_failed", {filename, error: e});
logError(e);
} else {
logger.debug("cleanup.deleted", {filename});
}
}); });
}); });
} }
+8 -6
View File
@@ -1,7 +1,9 @@
import {exec} from "node:child_process"; import {exec} from "node:child_process";
import {promisify} from "node:util"; import {promisify} from "node:util";
import {appLogger} from "../logging/logger";
const execAsync = promisify(exec); const execAsync = promisify(exec);
const logger = appLogger.child("shell-command-runner");
export type ShellCommandResult = { export type ShellCommandResult = {
stdout: string | null | undefined; stdout: string | null | undefined;
@@ -60,19 +62,19 @@ export class ShellCommandRunner {
maxBuffer: 64 * 1024, maxBuffer: 64 * 1024,
}); });
if (stdout) { if (stdout) {
console.log("COMMAND: ", command, "\n", "Output:", stdout); logger.debug("command.stdout", {command, stdout});
} }
if (stderr) { if (stderr) {
console.error("COMMAND: ", command, "\n", "Error:", stderr); logger.warn("command.stderr", {command, stderr});
} }
return {stdout, stderr}; return {stdout, stderr};
} catch (error: any) { } catch (error: unknown) {
console.error("Error code:", error.code); const err = error as Partial<Error & {code: unknown; stdout: string; stderr: string}>;
console.error("Stderr:", error.stderr); logger.error("command.failed", {command, code: err.code, stderr: err.stderr, error});
return {stdout: error.stdout ?? null, stderr: error.stderr ?? error.message}; return {stdout: err.stdout ?? null, stderr: err.stderr ?? err.message ?? String(error)};
} }
} }
+27 -3
View File
@@ -11,6 +11,10 @@
* queue always honors `parameters.retry_after` and requeues the task. * queue always honors `parameters.retry_after` and requeues the task.
*/ */
import {appLogger} from "../logging/logger";
const logger = appLogger.child("telegram-api-queue");
export type TelegramChatId = number | string; export type TelegramChatId = number | string;
export type TelegramChatType = string; export type TelegramChatType = string;
@@ -345,6 +349,7 @@ export class TelegramApiQueue {
this.globalBucket = new SlidingWindowRateLimit(this.options.globalLimit); this.globalBucket = new SlidingWindowRateLimit(this.options.globalLimit);
this.editBucket = new SlidingWindowRateLimit(this.options.editLimit); this.editBucket = new SlidingWindowRateLimit(this.options.editLimit);
this.bucketIdleMs = Math.max(this.options.perChatLimit.intervalMs, this.options.groupChatLimit.intervalMs) * 2; this.bucketIdleMs = Math.max(this.options.perChatLimit.intervalMs, this.options.groupChatLimit.intervalMs) * 2;
logger.debug("created", {maxConcurrent: this.options.maxConcurrent, maxAttempts: this.options.maxAttempts, maxQueueSize: this.options.maxQueueSize});
} }
get stats(): TelegramApiQueueStats { get stats(): TelegramApiQueueStats {
@@ -356,9 +361,18 @@ export class TelegramApiQueue {
} }
enqueue<T>(task: TelegramApiTask<T>, options: TelegramApiQueueTaskOptions = {}): Promise<T> { enqueue<T>(task: TelegramApiTask<T>, options: TelegramApiQueueTaskOptions = {}): Promise<T> {
if (this.closed) return Promise.reject(createClosedError()); if (this.closed) {
if (this.queue.length >= this.options.maxQueueSize) return Promise.reject(createQueueOverflowError(this.options.maxQueueSize)); logger.warn("enqueue.rejected.closed", {method: options.method, chatId: options.chatId});
if (options.signal?.aborted) return Promise.reject(createAbortError()); return Promise.reject(createClosedError());
}
if (this.queue.length >= this.options.maxQueueSize) {
logger.error("enqueue.rejected.overflow", {method: options.method, chatId: options.chatId, queued: this.queue.length, maxQueueSize: this.options.maxQueueSize});
return Promise.reject(createQueueOverflowError(this.options.maxQueueSize));
}
if (options.signal?.aborted) {
logger.debug("enqueue.rejected.aborted", {method: options.method, chatId: options.chatId});
return Promise.reject(createAbortError());
}
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const entry: QueueEntry<T> = { const entry: QueueEntry<T> = {
@@ -376,6 +390,7 @@ export class TelegramApiQueue {
this.attachAbortHandler(entry); this.attachAbortHandler(entry);
this.insertEntry(entry as QueueEntry<unknown>); this.insertEntry(entry as QueueEntry<unknown>);
logger.trace("enqueue.accepted", {taskId: entry.id, method: options.method, chatId: options.chatId, priority: options.priority, queued: this.queue.length, running: this.running});
this.pump(); this.pump();
}); });
} }
@@ -390,12 +405,14 @@ export class TelegramApiQueue {
close(reason: unknown = createClosedError()): void { close(reason: unknown = createClosedError()): void {
this.closed = true; this.closed = true;
logger.warn("closed", {queued: this.queue.length, running: this.running, reason});
if (this.timer) { if (this.timer) {
clearTimeout(this.timer); clearTimeout(this.timer);
this.timer = null; this.timer = null;
} }
const queued = this.queue; const queued = this.queue;
logger.debug("close.cancel_queued", {queued: queued.length});
this.queue = []; this.queue = [];
for (const entry of queued) { for (const entry of queued) {
this.cleanupAbortHandler(entry); this.cleanupAbortHandler(entry);
@@ -409,6 +426,7 @@ export class TelegramApiQueue {
clear(reason: unknown = new Error("Telegram API queue was cleared")): void { clear(reason: unknown = new Error("Telegram API queue was cleared")): void {
const queued = this.queue; const queued = this.queue;
logger.warn("cleared", {queued: queued.length, running: this.running, reason});
this.queue = []; this.queue = [];
for (const entry of queued) { for (const entry of queued) {
this.cleanupAbortHandler(entry); this.cleanupAbortHandler(entry);
@@ -435,6 +453,7 @@ export class TelegramApiQueue {
this.cleanupAbortHandler(entry); this.cleanupAbortHandler(entry);
entry.state = "cancelled"; entry.state = "cancelled";
logger.debug("entry.cancelled", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId});
entry.reject(createAbortError()); entry.reject(createAbortError());
this.resolveIdleIfNeeded(); this.resolveIdleIfNeeded();
} }
@@ -456,6 +475,7 @@ export class TelegramApiQueue {
} }
if (selection.delayMs > 0) { if (selection.delayMs > 0) {
logger.trace("pump.delayed", {delayMs: selection.delayMs, queued: this.queue.length, running: this.running});
this.schedule(selection.delayMs); this.schedule(selection.delayMs);
return; return;
} }
@@ -497,6 +517,7 @@ export class TelegramApiQueue {
this.cleanupAbortHandler(entry); this.cleanupAbortHandler(entry);
this.recordStart(entry, Date.now()); this.recordStart(entry, Date.now());
this.running++; this.running++;
logger.trace("entry.started", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt, queued: this.queue.length, running: this.running});
void this.runEntry(entry); void this.runEntry(entry);
} }
@@ -509,6 +530,7 @@ export class TelegramApiQueue {
signal: entry.options.signal, signal: entry.options.signal,
}); });
entry.state = "settled"; entry.state = "settled";
logger.trace("entry.settled", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt});
entry.resolve(result); entry.resolve(result);
} catch (error) { } catch (error) {
const retry = this.getRetryDecision(error, entry); const retry = this.getRetryDecision(error, entry);
@@ -523,6 +545,7 @@ export class TelegramApiQueue {
} else { } else {
this.attachAbortHandler(entry); this.attachAbortHandler(entry);
this.insertEntry(entry); this.insertEntry(entry);
logger.warn("entry.retry", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt - 1, delayMs: retry.delayMs, reason: retry.reason, error});
this.options.onRetry?.({ this.options.onRetry?.({
taskId: entry.id, taskId: entry.id,
method: entry.options.method, method: entry.options.method,
@@ -535,6 +558,7 @@ export class TelegramApiQueue {
} }
} else { } else {
entry.state = "settled"; entry.state = "settled";
logger.error("entry.failed", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt, error});
entry.reject(this.closed ? createClosedError() : error); entry.reject(this.closed ? createClosedError() : error);
} }
} finally { } finally {
+41 -24
View File
@@ -1,4 +1,5 @@
import * as si from "systeminformation"; import * as si from "systeminformation";
import {appLogger} from "../logging/logger";
import {Command} from "../base/command"; import {Command} from "../base/command";
import {CallbackCommand} from "../base/callback-command"; import {CallbackCommand} from "../base/callback-command";
import { import {
@@ -48,6 +49,9 @@ import {ShellCommandResult, ShellCommandRunner} from "./shell-command-runner";
const imageProcessingSemaphore = new AsyncSemaphore(2); const imageProcessingSemaphore = new AsyncSemaphore(2);
const fileWriteLocks = new KeyedAsyncLock(); const fileWriteLocks = new KeyedAsyncLock();
const logger = appLogger.child("utils");
const requirementLogger = appLogger.child("requirements");
const messageLogger = appLogger.child("messages");
export const ignore = () => { export const ignore = () => {
}; };
@@ -65,7 +69,7 @@ export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
}; };
export const logError = (e: Error | TelegramError | string | unknown) => { export const logError = (e: Error | TelegramError | string | unknown) => {
console.error(e); appLogger.error("error", {error: e});
}; };
export const errorPlaceholder = async (msg: Message) => { export const errorPlaceholder = async (msg: Message) => {
@@ -147,7 +151,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
!Environment.CHAT_IDS_WHITELIST.has(chatId) && !Environment.CHAT_IDS_WHITELIST.has(chatId) &&
!Environment.ADMIN_IDS.has(chatId) && !Environment.ADMIN_IDS.has(chatId) &&
!Environment.ADMIN_IDS.has(fromId)) { !Environment.ADMIN_IDS.has(fromId)) {
console.log(`${title}: chatId whitelist ignored.`); requirementLogger.debug("rejected.chat_whitelist", {title, chatId, fromId});
return false; return false;
} }
@@ -171,19 +175,19 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
}; };
if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) { if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) {
console.log(`${title}: creatorId is bad`); requirementLogger.debug("rejected.creator", {title, fromId});
await notifyUser(Environment.notBotCreatorText); await notifyUser(Environment.notBotCreatorText);
return false; return false;
} }
if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) { if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) {
console.log(`${title}: adminId is bad`); requirementLogger.debug("rejected.bot_admin", {title, fromId});
await notifyUser(Environment.notBotAdministratorText); await notifyUser(Environment.notBotAdministratorText);
return false; return false;
} }
if (reqs.isRequiresChat() && msg?.chat?.type === "private") { if (reqs.isRequiresChat() && msg?.chat?.type === "private") {
console.log(`${title}: chatId is bad`); requirementLogger.debug("rejected.chat_required", {title, chatId, chatType});
await notifyUser(Environment.notAChatText); await notifyUser(Environment.notAChatText);
return false; return false;
} }
@@ -192,7 +196,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
const member = await bot.getChatMember({chat_id: chatId, user_id: fromId}); const member = await bot.getChatMember({chat_id: chatId, user_id: fromId});
if (!isMemberAdmin(member)) { if (!isMemberAdmin(member)) {
console.log(`${title}: chatAdminId is bad`); requirementLogger.debug("rejected.chat_admin", {title, chatId, fromId});
await notifyUser(Environment.notChatAdministratorText); await notifyUser(Environment.notChatAdministratorText);
return false; return false;
} }
@@ -202,14 +206,14 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
const member = await bot.getChatMember({chat_id: chatId, user_id: botUser.id}); const member = await bot.getChatMember({chat_id: chatId, user_id: botUser.id});
if (!isMemberAdmin(member)) { if (!isMemberAdmin(member)) {
console.log(`${title}: botChatAdminId is bad`); requirementLogger.debug("rejected.bot_chat_admin", {title, chatId});
await notifyUser(Environment.botNotChatAdministratorText); await notifyUser(Environment.botNotChatAdministratorText);
return false; return false;
} }
} }
if (reqs.isRequiresReply() && !msg?.reply_to_message) { if (reqs.isRequiresReply() && !msg?.reply_to_message) {
console.log(`${title}: replyMessage is bad`); requirementLogger.debug("rejected.reply_required", {title, chatId, messageId});
await notifyUser(Environment.replyRequiredText); await notifyUser(Environment.replyRequiredText);
return false; return false;
} }
@@ -241,7 +245,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
} }
if (!originalFromId || (fromId !== originalFromId && fromId !== Environment.CREATOR_ID)) { if (!originalFromId || (fromId !== originalFromId && fromId !== Environment.CREATOR_ID)) {
console.log(`${title}: sameUser is bad`); requirementLogger.debug("rejected.same_user", {title, chatId, fromId, originalFromId});
await notifyUser(Environment.onlyOriginalAuthorText); await notifyUser(Environment.onlyOriginalAuthorText);
return false; return false;
} }
@@ -307,13 +311,13 @@ export async function editMessageText(options: EditOptions, retries = 1) {
} }
); );
return Promise.resolve(message); return Promise.resolve(message);
} catch (e: any) { } catch (e: unknown) {
logError(e); logError(e);
if (isMarkupFailed(e)) { if (isMarkupFailed(e as Error | TelegramError)) {
return Promise.resolve(true); return Promise.resolve(true);
} else if (isTooManyRequests(e) && retries > 0) { } else if (isTooManyRequests(e as Error | TelegramError) && retries > 0) {
const retryAfter = Number(e.message.split("retry after ")[1]) || 30; const retryAfter = Number((e instanceof Error ? e.message : String(e)).split("retry after ")[1]) || 30;
await delay(retryAfter * 1000); await delay(retryAfter * 1000);
return editMessageText(options, retries - 1); return editMessageText(options, retries - 1);
} else { } else {
@@ -1836,9 +1840,10 @@ export function startIntervalEditor(params: {
try { try {
await params.editFn(next); await params.editFn(next);
lastSent = next; lastSent = next;
} catch (e: any) { } catch (e: unknown) {
if ((e?.description ?? e?.message ?? "").includes("message is not modified")) return; const description = e instanceof Error ? e.message : String(e);
logError("edit failed: " + e); if (description.includes("message is not modified")) return;
logError("edit failed: " + description);
} }
}; };
@@ -1896,7 +1901,6 @@ type RuntimeInfo =
| { runtime: "unknown"; version: string }; | { runtime: "unknown"; version: string };
export function getRuntimeInfo(): RuntimeInfo { export function getRuntimeInfo(): RuntimeInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const v = process.versions ?? {}; const v = process.versions ?? {};
if (typeof v.bun === "string") { if (typeof v.bun === "string") {
@@ -1906,7 +1910,6 @@ export function getRuntimeInfo(): RuntimeInfo {
return {runtime: "node", version: v.node}; return {runtime: "node", version: v.node};
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return {runtime: "unknown", version: String(process.version ?? "")}; return {runtime: "unknown", version: String(process.version ?? "")};
} }
@@ -1957,7 +1960,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 { export function ifTrue(exp?: string | number | boolean): boolean {
if (!exp) return false; if (!exp) return false;
@@ -2091,19 +2093,23 @@ export function photoPathByUniqueId(uniqueId: string): string {
} }
export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> { export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
console.log("my_chat_member", u); messageLogger.debug("my_chat_member", {update: u});
} }
export async function processGuestMessage(msg: Message): Promise<void> { export async function processGuestMessage(msg: Message): Promise<void> {
// return processNewMessage(msg, true); // return processNewMessage(msg, true);
console.log("NEW_GUEST_MESSAGE", msg); messageLogger.debug("guest_message.received", {message: msg});
} }
export async function processNewMessage(msg: Message, isGuest?: boolean): Promise<void> { export async function processNewMessage(msg: Message, isGuest?: boolean): Promise<void> {
console.log(isGuest ? "NEW_GUEST_MESSAGE" : "NEW_MESSAGE", msg); messageLogger.debug(isGuest ? "guest_message.received" : "message.received", {message: msg});
if (!msg.from) return; if (!msg.from) {
messageLogger.debug("message.skipped.no_sender", {chatId: msg.chat?.id, messageId: msg.message_id});
return;
}
const startedAt = Date.now();
const from = msg.from; const from = msg.from;
Environment.reloadRuntimeConfigIfChanged(); Environment.reloadRuntimeConfigIfChanged();
@@ -2116,6 +2122,7 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
UserStore.put(from) UserStore.put(from)
] ]
); );
messageLogger.debug("message.persisted", {chatId: msg.chat.id, messageId: msg.message_id, fromId: from.id, duration: logger.duration(startedAt)});
storedMsg = results[0]; storedMsg = results[0];
locale = await resolveInterfaceLocaleForUser(from.id, from.language_code); locale = await resolveInterfaceLocaleForUser(from.id, from.language_code);
@@ -2194,7 +2201,15 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
const hasAudioAttachment = !!msg.voice || !!msg.audio || !!msg.document?.mime_type?.startsWith("audio/") const hasAudioAttachment = !!msg.voice || !!msg.audio || !!msg.document?.mime_type?.startsWith("audio/")
|| !!msg.video_note; || !!msg.video_note;
const hasImageAttachment = !!msg.photo?.length || !!msg.document?.mime_type?.startsWith("image/"); const hasImageAttachment = !!msg.photo?.length || !!msg.document?.mime_type?.startsWith("image/");
if (executed || (!cmdText && !hasAudioAttachment && !hasImageAttachment)) return; if (executed) {
messageLogger.debug("message.command_executed", {chatId: msg.chat.id, messageId: msg.message_id, command: cmd?.title});
return;
}
if (!cmdText && !hasAudioAttachment && !hasImageAttachment) {
messageLogger.debug("message.skipped.empty", {chatId: msg.chat.id, messageId: msg.message_id});
return;
}
const hasConfiguredPrefix = Environment.BOT_PREFIX.length > 0; const hasConfiguredPrefix = Environment.BOT_PREFIX.length > 0;
const startsWithPrefix = hasConfiguredPrefix && cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase()); const startsWithPrefix = hasConfiguredPrefix && cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase());
@@ -2224,12 +2239,14 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
}); });
if (!isReplyToBot && !hasPrefix && !hasBotMention && !hasAudioAttachment) { if (!isReplyToBot && !hasPrefix && !hasBotMention && !hasAudioAttachment) {
messageLogger.debug("message.skipped.not_addressed", {chatId: msg.chat.id, messageId: msg.message_id});
return; return;
} }
} }
const provider = await resolveEffectiveAiProviderForUser(from.id); const provider = await resolveEffectiveAiProviderForUser(from.id);
messageLogger.info("ai.dispatch", {chatId: msg.chat.id, messageId: msg.message_id, fromId: from.id, provider});
void runUnifiedAi({ void runUnifiedAi({
provider: provider, provider: provider,
msg: msg, msg: msg,
+16 -8
View File
@@ -1,24 +1,32 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "ESNext",
"moduleResolution": "NodeNext", // Modern resolution "moduleResolution": "Bundler",
"rootDir": "src", // Limits scope "rootDir": "src",
"outDir": "dist", "outDir": "dist",
"incremental": true, // HUGE performance boost "incremental": true,
"isolatedModules": true, // Ensures compatibility with fast runners "isolatedModules": true,
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"types": [
"node",
"bun"
]
}, },
"include": ["src/**/*.ts"], "include": [
"exclude": ["node_modules", "dist"] // Explicitly exclude build artifacts "src/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
} }