diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b5dab54 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + build-runtime: + name: Build (Runtime) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install root dev dependencies + run: npm ci --ignore-scripts + + - name: Install plugin dependencies + run: | + for pkg in plugins/*/package.json; do + dir=$(dirname "$pkg") + if [ -f "$dir/package-lock.json" ]; then + npm ci --ignore-scripts --no-audit --no-fund --prefix "$dir" + fi + done + + - name: Validate plugins load (Runtime) + run: node scripts/validate-plugins.mjs + + build-sdk: + name: Build (SDK with DTS) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install root dev dependencies + run: npm ci --ignore-scripts + + - name: Install plugin dependencies + run: | + for pkg in plugins/*/package.json; do + dir=$(dirname "$pkg") + if [ -f "$dir/package-lock.json" ]; then + npm ci --ignore-scripts --no-audit --no-fund --prefix "$dir" + fi + done + + - name: Build SDK plugins and generate type declarations + run: node scripts/build-sdk.mjs + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install root dev dependencies + run: npm ci --ignore-scripts + + - name: Lint + run: npm run lint + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install root dev dependencies + run: npm ci --ignore-scripts + + - name: Install plugin dependencies + run: | + for pkg in plugins/*/package.json; do + dir=$(dirname "$pkg") + if [ -f "$dir/package-lock.json" ]; then + npm ci --ignore-scripts --no-audit --no-fund --prefix "$dir" + fi + done + + - name: Run tests + run: node scripts/run-tests.mjs + + typescript: + name: TypeScript + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install root dev dependencies + run: npm ci --ignore-scripts + + - name: TypeScript type check + run: npm run typecheck diff --git a/.github/workflows/deploy-vercel.yml b/.github/workflows/deploy-vercel.yml new file mode 100644 index 0000000..0191725 --- /dev/null +++ b/.github/workflows/deploy-vercel.yml @@ -0,0 +1,62 @@ +name: Deploy to Vercel + +on: + pull_request: + push: + branches: [main] + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + steps: + - name: Check Vercel token + id: check + run: | + if [ -z "$VERCEL_TOKEN" ]; then + echo "available=false" >> "$GITHUB_OUTPUT" + echo "⚠️ VERCEL_TOKEN is not configured — skipping deployment" + else + echo "available=true" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@v4 + if: steps.check.outputs.available == 'true' + + - uses: actions/setup-node@v4 + if: steps.check.outputs.available == 'true' + with: + node-version: "20" + + - name: Install Vercel CLI + if: steps.check.outputs.available == 'true' + run: npm install --global vercel@latest + + - name: Pull Vercel environment + if: steps.check.outputs.available == 'true' + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build project artifacts + if: steps.check.outputs.available == 'true' + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy to Vercel (preview) + if: steps.check.outputs.available == 'true' + id: deploy + run: | + url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}) + echo "url=$url" >> "$GITHUB_OUTPUT" + + - name: Comment preview URL on PR + if: steps.check.outputs.available == 'true' && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `🚀 Preview deployed: ${{ steps.deploy.outputs.url }}` + }) diff --git a/.gitignore b/.gitignore index 9d3b866..eb2d7d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +dist/ .DS_Store *.log diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..c46a5b9 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7ebc699 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,72 @@ +// @ts-check +import js from "@eslint/js"; + +/** @type {import("eslint").Linter.Config[]} */ +export default [ + js.configs.recommended, + { + files: ["plugins/**/*.js", "scripts/**/*.mjs"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + // Node.js globals + process: "readonly", + console: "readonly", + Buffer: "readonly", + URL: "readonly", + URLSearchParams: "readonly", + fetch: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + __dirname: "readonly", + __filename: "readonly", + // Web/Node globals available in Node 18+ + AbortSignal: "readonly", + AbortController: "readonly", + TextDecoder: "readonly", + TextEncoder: "readonly", + ReadableStream: "readonly", + WritableStream: "readonly", + TransformStream: "readonly", + FormData: "readonly", + Headers: "readonly", + Request: "readonly", + Response: "readonly", + Blob: "readonly", + File: "readonly", + Event: "readonly", + EventTarget: "readonly", + MessageChannel: "readonly", + MessageEvent: "readonly", + crypto: "readonly", + performance: "readonly", + structuredClone: "readonly", + queueMicrotask: "readonly", + atob: "readonly", + btoa: "readonly", + }, + }, + rules: { + // Errors — these indicate broken code + "no-undef": "error", + "no-console": "off", + + // Warnings — code quality issues, won't fail CI + "no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "no-var": "warn", + "prefer-const": "warn", + }, + }, + { + // Ignore generated files and node_modules + ignores: [ + "node_modules/**", + "plugins/*/node_modules/**", + "dist/**", + "**/*.min.js", + ], + }, +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2a7931e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2035 @@ +{ + "name": "teleton-plugins", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "teleton-plugins", + "version": "1.0.0", + "devDependencies": { + "@eslint/js": "^9.0.0", + "@ton/core": "^0.63.0", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^16.0.0", + "eslint": "^9.0.0", + "telegram": "^2.26.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@cryptography/aes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz", + "integrity": "sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==", + "dev": true, + "license": "GPL-3.0-or-later" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ton/core": { + "version": "0.63.1", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", + "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/crypto": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", + "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ton/crypto-primitives": "2.1.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", + "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", + "dev": true, + "license": "MIT", + "dependencies": { + "jssha": "3.2.0" + } + }, + "node_modules/@ton/ton": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-16.2.2.tgz", + "integrity": "sha512-yEOw4IW3gpRZxJAcILMI4dQ1d5/eAAbD2VU/Iwc6z7f2jt1mLDWVED8yn2vLNucQfZr+1eaqYHLztYVFZ7PKmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.6.7", + "dataloader": "^2.0.0", + "zod": "^3.21.4" + }, + "peerDependencies": { + "@ton/core": ">=0.63.0 <1.0.0", + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "write-file-atomic": "^1.1.4" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/real-cancellable-promise": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.3.tgz", + "integrity": "sha512-hBI5Gy/55VEeeMtImMgEirD7eq5UmqJf1J8dFZtbJZA/3rB0pYFZ7PayMGueb6v4UtUtpKpP+05L0VwyE1hI9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/store2": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", + "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/telegram": { + "version": "2.26.22", + "resolved": "https://registry.npmjs.org/telegram/-/telegram-2.26.22.tgz", + "integrity": "sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cryptography/aes": "^0.1.1", + "async-mutex": "^0.3.0", + "big-integer": "^1.6.48", + "buffer": "^6.0.3", + "htmlparser2": "^6.1.0", + "mime": "^3.0.0", + "node-localstorage": "^2.2.1", + "pako": "^2.0.3", + "path-browserify": "^1.0.1", + "real-cancellable-promise": "^1.1.1", + "socks": "^2.6.2", + "store2": "^2.13.0", + "ts-custom-error": "^3.2.0", + "websocket": "^1.0.34" + }, + "optionalDependencies": { + "bufferutil": "^4.0.3", + "utf-8-validate": "^5.0.5" + } + }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1519b8c --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "teleton-plugins", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Community plugin directory for Teleton — the Telegram AI agent on TON", + "scripts": { + "validate": "node scripts/validate-plugins.mjs", + "lint": "eslint \"plugins/**/*.js\" \"scripts/**/*.mjs\"", + "test": "node scripts/run-tests.mjs", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@eslint/js": "^9.0.0", + "@ton/core": "^0.63.0", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^16.0.0", + "eslint": "^9.0.0", + "telegram": "^2.26.0", + "typescript": "^5.7.0" + } +} diff --git a/plugins/giftindex/manifest.json b/plugins/giftindex/manifest.json index 6429bd9..5de68ed 100644 --- a/plugins/giftindex/manifest.json +++ b/plugins/giftindex/manifest.json @@ -55,6 +55,7 @@ "description": "View market state, corridors, corridor advisory (cancel+re-place or wait), and top collections" } ], + "permissions": [], "tags": ["giftindex", "ton", "trading", "telegram-gifts", "index"], "repository": "https://github.com/TONresistor/teleton-plugins", "funding": null diff --git a/plugins/github-dev-assistant/.gitignore b/plugins/github-dev-assistant/.gitignore new file mode 100644 index 0000000..764d540 --- /dev/null +++ b/plugins/github-dev-assistant/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.env.* +*.local diff --git a/plugins/github-dev-assistant/CHANGELOG.md b/plugins/github-dev-assistant/CHANGELOG.md new file mode 100644 index 0000000..b792a15 --- /dev/null +++ b/plugins/github-dev-assistant/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to `github-dev-assistant` are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-03-17 + +### Added +- Initial release of the `github-dev-assistant` plugin +- **Authorization (1 tool)** + - `github_check_auth` — verify current authentication status via Personal Access Token +- **Repository management (2 tools)** + - `github_list_repos` — list user or organization repositories with filtering + - `github_create_repo` — create new repositories with optional license and gitignore +- **File & commit operations (3 tools)** + - `github_get_file` — read files or list directories (base64 decode handled automatically) + - `github_update_file` — create or update files with commits (base64 encode handled automatically) + - `github_create_branch` — create branches from any ref +- **Pull request management (3 tools)** + - `github_create_pr` — create pull requests with draft support + - `github_list_prs` — list PRs with state, head, base, and sort filtering + - `github_merge_pr` — merge PRs with `require_pr_review` confirmation policy +- **Issue management (4 tools)** + - `github_create_issue` — create issues with labels, assignees, and milestone + - `github_list_issues` — list issues with extensive filtering options + - `github_comment_issue` — add comments to issues and PRs + - `github_close_issue` — close issues/PRs with optional comment and reason +- **GitHub Actions (1 tool)** + - `github_trigger_workflow` — dispatch workflow_dispatch events with inputs +- **Security** + - All tokens stored exclusively via `sdk.secrets` + - Token redaction in error messages + - `require_pr_review` confirmation policy for destructive merge operations diff --git a/plugins/github-dev-assistant/README.md b/plugins/github-dev-assistant/README.md new file mode 100644 index 0000000..f411383 --- /dev/null +++ b/plugins/github-dev-assistant/README.md @@ -0,0 +1,176 @@ +# GitHub Dev Assistant + +Full GitHub development workflow automation for the [Teleton](https://github.com/xlabtg/teleton-agent) AI agent. Enables autonomous creation of repositories, files, branches, pull requests, issues, and workflow triggers — all from a Telegram chat. + +## Features + +| Category | Tools | +|----------|-------| +| **Authorization** | `github_check_auth` | +| **Repositories** | `github_list_repos`, `github_create_repo` | +| **Files & Branches** | `github_get_file`, `github_update_file`, `github_create_branch` | +| **Pull Requests** | `github_create_pr`, `github_list_prs`, `github_merge_pr` | +| **Issues** | `github_create_issue`, `github_list_issues`, `github_comment_issue`, `github_close_issue` | +| **GitHub Actions** | `github_trigger_workflow` | + +**14 tools total** covering the complete GitHub development lifecycle. + +## Installation + +### Via Teleton Web UI +1. Open the Teleton Web UI and navigate to **Plugins**. +2. Search for `github-dev-assistant` and click **Install**. +3. Open plugin **Settings** to configure the Personal Access Token. + +### Manual Installation + +```bash +mkdir -p ~/.teleton/plugins +cp -r plugins/github-dev-assistant ~/.teleton/plugins/ +``` + +## Setup & Authorization + +### Step 1: Create a Personal Access Token + +1. Go to **GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)** +2. Click **Generate new token (classic)** +3. Select scopes: `repo`, `workflow`, `user` +4. Click **Generate token** and copy the token + +### Step 2: Configure Plugin Secret + +Set the token via environment variable or Teleton secrets store: + +| Secret | Environment Variable | Description | +|--------|---------------------|-------------| +| `github_token` | `GITHUB_DEV_ASSISTANT_GITHUB_TOKEN` | GitHub Personal Access Token | + +### Step 3: Verify Authorization + +In the agent chat: +``` +Check my GitHub auth status +``` + +## Usage Examples + +### Check Authorization +``` +Check my GitHub auth status +``` + +### Repository Operations +``` +List my GitHub repos +List repos for the organization my-org, sorted by stars +Create a private GitHub repo called my-new-project with a MIT license +``` + +### File Operations +``` +Get the contents of README.md from octocat/hello-world +Read src/index.js from my-org/my-repo on the develop branch +Update README.md in octocat/hello with content "# Hello World" and commit message "Update docs" +Create a new file docs/api.md in my-org/my-repo with the API documentation content +``` + +### Branch Operations +``` +Create branch feat/login-ui from main in my-org/my-repo +Create a hotfix branch from the v2.1.0 tag in my-org/production-app +``` + +### Pull Request Operations +``` +Create a PR in my-org/my-repo from branch feat/login-ui to main with title "Add login UI" +List open PRs in my-org/my-repo +List all PRs (open and closed) in octocat/hello +Merge PR #42 in my-org/my-repo using squash strategy +``` + +### Issue Operations +``` +Create an issue in my-org/my-repo: title "Bug: login fails on Safari", label it with "bug" and "priority-high" +List open issues in my-org/my-repo assigned to me +Comment on issue #15 in my-org/my-repo: "Fixed in PR #42" +Close issue #15 in my-org/my-repo as completed +``` + +### GitHub Actions +``` +Trigger the deploy.yml workflow on the main branch in my-org/my-repo +Run CI workflow on branch feat/new-feature in my-org/my-repo with input environment=staging +``` + +## Configuration + +```yaml +# ~/.teleton/config.yaml +plugins: + github_dev_assistant: + default_owner: null # Default GitHub username/org for operations + default_branch: "main" # Default branch for commits and PRs + require_pr_review: false # Require confirmation before merging PRs + commit_author_name: "Teleton AI Agent" # Author name in commits + commit_author_email: "agent@teleton.local" # Author email in commits +``` + +## Security Best Practices + +- **Never share your Personal Access Token.** It is stored encrypted via `sdk.secrets` and never appears in logs. +- **Enable `require_pr_review`** if you want human confirmation before any PR merges. +- **Use minimum required scopes.** `repo`, `workflow`, and `user` cover all plugin features; remove `workflow` if you don't need GitHub Actions. +- **Review commit author settings** — commits will be attributed to the configured name/email, not your personal GitHub account. + +## Tool Reference + +### `github_check_auth` +Check whether the plugin is authenticated and return the connected user's login. + +### `github_list_repos` +List repositories. Parameters: `owner`, `type`, `sort`, `direction`, `per_page`, `page`. + +### `github_create_repo` +Create a new repository. Parameters: `name` (required), `description`, `private`, `auto_init`, `license_template`, `gitignore_template`. + +### `github_get_file` +Read a file or list a directory. Parameters: `owner`, `repo`, `path` (all required), `ref`. + +### `github_update_file` +Create or update a file with a commit. Parameters: `owner`, `repo`, `path`, `content`, `message` (all required), `branch`, `sha` (required for updates), `committer_name`, `committer_email`. + +### `github_create_branch` +Create a new branch. Parameters: `owner`, `repo`, `branch` (all required), `from_ref`. + +### `github_create_pr` +Create a pull request. Parameters: `owner`, `repo`, `title`, `head` (all required), `body`, `base`, `draft`, `maintainer_can_modify`. + +### `github_list_prs` +List pull requests. Parameters: `owner`, `repo` (required), `state`, `head`, `base`, `sort`, `direction`, `per_page`, `page`. + +### `github_merge_pr` +Merge a pull request. Parameters: `owner`, `repo`, `pr_number` (all required), `merge_method`, `commit_title`, `commit_message`, `confirmed`. + +### `github_create_issue` +Create an issue. Parameters: `owner`, `repo`, `title` (all required), `body`, `labels`, `assignees`, `milestone`. + +### `github_list_issues` +List issues. Parameters: `owner`, `repo` (required), `state`, `labels`, `assignee`, `creator`, `mentioned`, `sort`, `direction`, `per_page`, `page`. + +### `github_comment_issue` +Add a comment. Parameters: `owner`, `repo`, `issue_number`, `body` (all required). + +### `github_close_issue` +Close an issue or PR. Parameters: `owner`, `repo`, `issue_number` (all required), `comment`, `reason`. + +### `github_trigger_workflow` +Trigger a GitHub Actions workflow dispatch. Parameters: `owner`, `repo`, `workflow_id`, `ref` (all required), `inputs`. + +## Developer + +**Developer:** [xlabtg](https://github.com/xlabtg) + +## License + +MIT — see [LICENSE](../../LICENSE) diff --git a/plugins/github-dev-assistant/index.js b/plugins/github-dev-assistant/index.js new file mode 100644 index 0000000..2593034 --- /dev/null +++ b/plugins/github-dev-assistant/index.js @@ -0,0 +1,149 @@ +/** + * github-dev-assistant — Full GitHub Development Workflow Automation + * + * Provides 14 tools for autonomous GitHub operations: + * Auth (1): github_check_auth + * Repos (2): github_list_repos, github_create_repo + * Files (3): github_get_file, github_update_file, github_create_branch + * PRs (3): github_create_pr, github_list_prs, github_merge_pr + * Issues (4): github_create_issue, github_list_issues, github_comment_issue, github_close_issue + * Actions (1): github_trigger_workflow + * + * Authentication: + * - Uses a Personal Access Token (PAT) stored in sdk.secrets as "github_token" + * - Set GITHUB_DEV_ASSISTANT_GITHUB_TOKEN env var or use the secrets store + * + * Security: + * - All tokens stored exclusively in sdk.secrets + * - No tokens, secrets, or sensitive data in sdk.log output + * - Destructive operations (merge) respect require_pr_review policy + * + * Usage: + * 1. Set github_token in plugin secrets (Personal Access Token from github.com/settings/tokens) + * 2. Call github_check_auth to verify authorization + * 3. Use any of the remaining tools + */ + +import { buildRepoOpsTools } from "./lib/repo-ops.js"; +import { buildPRManagerTools } from "./lib/pr-manager.js"; +import { buildIssueTrackerTools } from "./lib/issue-tracker.js"; +import { createGitHubClient } from "./lib/github-client.js"; +import { formatError } from "./lib/utils.js"; + +// --------------------------------------------------------------------------- +// Inline manifest — read by the Teleton runtime for SDK version gating, +// defaultConfig merging, and secrets registration. +// --------------------------------------------------------------------------- + +export const manifest = { + name: "github-dev-assistant", + version: "1.0.0", + sdkVersion: ">=1.0.0", + description: + "Full GitHub development workflow automation — repos, files, branches, PRs, issues, and GitHub Actions via Personal Access Token", + secrets: { + github_token: { + required: true, + env: "GITHUB_DEV_ASSISTANT_GITHUB_TOKEN", + description: "GitHub Personal Access Token (create at https://github.com/settings/tokens)", + }, + }, + defaultConfig: { + default_owner: null, + default_branch: "main", + require_pr_review: false, + commit_author_name: "Teleton AI Agent", + commit_author_email: "agent@teleton.local", + }, +}; + +// --------------------------------------------------------------------------- +// SDK export — Teleton runtime calls tools(sdk) and uses the returned array +// --------------------------------------------------------------------------- + +export const tools = (sdk) => { + // --------------------------------------------------------------------------- + // Auth tools (1) + // --------------------------------------------------------------------------- + + const authTools = [ + // ------------------------------------------------------------------------- + // Tool: github_check_auth + // ------------------------------------------------------------------------- + { + name: "github_check_auth", + description: + "Use this when the user wants to check if GitHub is connected or verify the GitHub account. " + + "Returns the authenticated GitHub username and confirms the token works.", + category: "data-bearing", + parameters: { + type: "object", + properties: {}, + }, + execute: async (_params, _context) => { + try { + const client = createGitHubClient(sdk); + if (!client.isAuthenticated()) { + return { + success: true, + data: { + authenticated: false, + message: + "GitHub is not connected. Please set the github_token secret with your Personal Access Token. " + + "You can create one at https://github.com/settings/tokens", + }, + }; + } + const user = await client.get("/user"); + sdk.log.info(`github_check_auth: authenticated as ${user.login}`); + return { + success: true, + data: { + authenticated: true, + message: `GitHub is connected. Authenticated as @${user.login} (${user.name ?? user.login}).`, + login: user.login, + name: user.name ?? null, + }, + }; + } catch (err) { + if (err.status === 401) { + return { + success: true, + data: { + authenticated: false, + message: + "GitHub token is invalid or expired. Please update the github_token secret with a valid Personal Access Token.", + }, + }; + } + return { success: false, error: `GitHub auth check failed: ${formatError(err)}` }; + } + }, + }, + ]; + + // --------------------------------------------------------------------------- + // Repository, file, and branch tools (5) + // --------------------------------------------------------------------------- + const repoTools = buildRepoOpsTools(sdk); + + // --------------------------------------------------------------------------- + // Pull request tools (3) + // --------------------------------------------------------------------------- + const prTools = buildPRManagerTools(sdk); + + // --------------------------------------------------------------------------- + // Issue and workflow tools (5) + // --------------------------------------------------------------------------- + const issueTools = buildIssueTrackerTools(sdk); + + // --------------------------------------------------------------------------- + // Combine and return all 14 tools + // --------------------------------------------------------------------------- + return [ + ...authTools, // 1: github_check_auth + ...repoTools, // 5: github_list_repos, github_create_repo, github_get_file, github_update_file, github_create_branch + ...prTools, // 3: github_create_pr, github_list_prs, github_merge_pr + ...issueTools, // 5: github_create_issue, github_list_issues, github_comment_issue, github_close_issue, github_trigger_workflow + ]; +}; diff --git a/plugins/github-dev-assistant/lib/github-client.js b/plugins/github-dev-assistant/lib/github-client.js new file mode 100644 index 0000000..db3a377 --- /dev/null +++ b/plugins/github-dev-assistant/lib/github-client.js @@ -0,0 +1,220 @@ +/** + * GitHub REST API client for the github-dev-assistant plugin. + * + * Wraps the GitHub REST API v3 with: + * - Automatic Authorization header injection from sdk.secrets at request time + * - Rate-limit tracking and soft throttling + * - Structured error handling with no token leakage in logs + * - Pagination support via Link header parsing + * + * Usage: + * const client = createGitHubClient(sdk); + * const data = await client.get("/user/repos"); + * + * The client reads the token from sdk.secrets on every request, so stale + * client instances automatically pick up updated tokens. + */ + +import { formatError, createRateLimiter, parseLinkHeader } from "./utils.js"; + +const GITHUB_API_BASE = "https://api.github.com"; + +// GitHub recommends no more than ~60 secondary rate-limit requests per minute +// for unauthenticated, and ~5000/hour for authenticated. We throttle lightly. +const MIN_REQUEST_DELAY_MS = 100; + +/** + * Create a GitHub API client bound to the given sdk instance. + * + * @param {object} sdk - Teleton plugin SDK + * @returns {object} Client with get(), post(), put(), patch(), delete() methods + */ +export function createGitHubClient(sdk) { + const rateLimiter = createRateLimiter(MIN_REQUEST_DELAY_MS); + + /** + * Retrieve the stored Personal Access Token from sdk.secrets. + * Returns null if not set (unauthenticated). + * @returns {string|null} + */ + function getAccessToken() { + return sdk.secrets.get("github_token") ?? null; + } + + /** + * Build common request headers. + * Token is read at request time — never at client creation time. + * @returns {object} + */ + function buildHeaders(extraHeaders = {}) { + const token = getAccessToken(); + const headers = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Content-Type": "application/json", + "User-Agent": "teleton-github-dev-assistant/1.0.0", + ...extraHeaders, + }; + if (token) { + // Token injected at request time — never logged + headers.Authorization = `Bearer ${token}`; + } + return headers; + } + + /** + * Core fetch wrapper. Applies rate limiting, injects auth, handles errors. + * + * @param {string} method - HTTP method + * @param {string} path - API path (e.g. "/repos/owner/repo") + * @param {object|null} body - JSON body for POST/PUT/PATCH + * @param {object} queryParams - URL query parameters + * @returns {Promise<{ data: any, headers: Headers, status: number }>} + * @throws {Error} On non-2xx responses with structured message + */ + async function request(method, path, body = null, queryParams = {}) { + await rateLimiter.wait(); + + const url = new URL(path.startsWith("http") ? path : `${GITHUB_API_BASE}${path}`); + for (const [key, value] of Object.entries(queryParams)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + } + + const opts = { + method: method.toUpperCase(), + headers: buildHeaders(), + signal: AbortSignal.timeout(20000), + }; + + if (body !== null && ["POST", "PUT", "PATCH"].includes(opts.method)) { + opts.body = JSON.stringify(body); + } + + const res = await fetch(url.toString(), opts); + + // 204 No Content — success with no body + if (res.status === 204) { + return { data: null, headers: res.headers, status: res.status }; + } + + const responseText = await res.text(); + let responseData; + try { + responseData = JSON.parse(responseText); + } catch { + responseData = responseText; + } + + if (!res.ok) { + // Build a clear, non-leaking error message + const ghMessage = + typeof responseData === "object" && responseData?.message + ? responseData.message + : responseText.slice(0, 200); + + // Map common GitHub status codes to helpful messages + const statusMessages = { + 401: "Not authenticated. Please set the github_token secret with a valid Personal Access Token.", + 403: `Access denied. ${ghMessage}`, + 404: `Not found. ${ghMessage}`, + 409: `Conflict. ${ghMessage}`, + 422: `Validation error. ${ghMessage}`, + 429: "GitHub API rate limit exceeded. Please wait before retrying.", + }; + + const message = statusMessages[res.status] ?? `GitHub API error ${res.status}: ${ghMessage}`; + const err = new Error(message); + err.status = res.status; + err.githubData = responseData; + throw err; + } + + return { data: responseData, headers: res.headers, status: res.status }; + } + + return { + /** + * GET request to GitHub API. + * @param {string} path + * @param {object} [queryParams] + * @returns {Promise} Response data + */ + async get(path, queryParams = {}) { + const { data } = await request("GET", path, null, queryParams); + return data; + }, + + /** + * GET with pagination — returns data and pagination metadata. + * @param {string} path + * @param {object} [queryParams] + * @returns {Promise<{ data: any, pagination: object }>} + */ + async getPaginated(path, queryParams = {}) { + const { data, headers } = await request("GET", path, null, queryParams); + const linkHeader = headers.get("Link"); + return { data, pagination: parseLinkHeader(linkHeader) }; + }, + + /** + * POST request. + * @param {string} path + * @param {object} body + * @returns {Promise} + */ + async post(path, body) { + const { data } = await request("POST", path, body); + return data; + }, + + /** + * PUT request. + * @param {string} path + * @param {object} body + * @returns {Promise} + */ + async put(path, body) { + const { data } = await request("PUT", path, body); + return data; + }, + + /** + * PATCH request. + * @param {string} path + * @param {object} body + * @returns {Promise} + */ + async patch(path, body) { + const { data } = await request("PATCH", path, body); + return data; + }, + + /** + * DELETE request. + * @param {string} path + * @returns {Promise} + */ + async delete(path) { + const { data } = await request("DELETE", path); + return data; + }, + + /** + * POST with raw response (for workflow dispatches etc.) + * @param {string} path + * @param {object} body + * @returns {Promise<{ status: number, data: any }>} + */ + async postRaw(path, body) { + const { status, data } = await request("POST", path, body); + return { status, data }; + }, + + /** Check if authenticated (token is present in secrets) */ + isAuthenticated() { + return !!getAccessToken(); + }, + }; +} diff --git a/plugins/github-dev-assistant/lib/issue-tracker.js b/plugins/github-dev-assistant/lib/issue-tracker.js new file mode 100644 index 0000000..fb3baca --- /dev/null +++ b/plugins/github-dev-assistant/lib/issue-tracker.js @@ -0,0 +1,495 @@ +/** + * Issue management for the github-dev-assistant plugin. + * + * Covers: + * - github_create_issue — create a new issue + * - github_list_issues — list issues with filtering + * - github_comment_issue — add a comment to an issue or PR + * - github_close_issue — close an issue or PR with optional comment + * - github_trigger_workflow — trigger a GitHub Actions workflow + * + * All tools create a fresh GitHub client per execution to pick up the latest + * token from sdk.secrets (avoids stale client issues). + * + * All tools return { success, data?, error? } per the SDK ToolResult contract. + */ + +import { createGitHubClient } from "./github-client.js"; +import { validateRequired, validateEnum, clampInt, formatError } from "./utils.js"; + +/** + * Build issue tracking and workflow tools. + * + * @param {object} sdk - Teleton plugin SDK + * @returns {object[]} Array of tool definitions + */ +export function buildIssueTrackerTools(sdk) { + return [ + // ------------------------------------------------------------------------- + // Tool: github_create_issue + // ------------------------------------------------------------------------- + { + name: "github_create_issue", + description: + "Use this when the user wants to create a new issue or bug report in a GitHub repository. " + + "Returns the issue number and URL.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + title: { + type: "string", + description: "Issue title (required)", + }, + body: { + type: "string", + description: "Issue description (Markdown supported)", + }, + labels: { + type: "array", + items: { type: "string" }, + description: "Labels to apply (must exist in the repository)", + }, + assignees: { + type: "array", + items: { type: "string" }, + description: "GitHub usernames to assign to this issue", + }, + milestone: { + type: "integer", + description: "Milestone number to associate with the issue", + }, + }, + required: ["owner", "repo", "title"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "title"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const body = { title: params.title }; + if (params.body) body.body = params.body; + if (Array.isArray(params.labels) && params.labels.length > 0) { + body.labels = params.labels; + } + if (Array.isArray(params.assignees) && params.assignees.length > 0) { + body.assignees = params.assignees; + } + if (params.milestone) body.milestone = params.milestone; + + const issue = await client.post( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/issues`, + body + ); + + sdk.log.info( + `github_create_issue: created issue #${issue.number} in ${params.owner}/${params.repo}` + ); + + return { + success: true, + data: { + number: issue.number, + title: issue.title, + html_url: issue.html_url, + labels: issue.labels?.map((l) => (typeof l === "string" ? l : l.name)) ?? [], + }, + }; + } catch (err) { + return { success: false, error: `Failed to create issue: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_list_issues + // ------------------------------------------------------------------------- + { + name: "github_list_issues", + description: + "Use this when the user wants to see open issues or a list of bugs/tasks in a GitHub repository. " + + "Returns a formatted list of issues with title, author, labels, and URL.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + state: { + type: "string", + enum: ["open", "closed", "all"], + description: "Filter by issue state (default: open)", + }, + labels: { + type: "array", + items: { type: "string" }, + description: "Filter by label names", + }, + assignee: { + type: "string", + description: "Filter by assignee username. Use '*' for any assigned.", + }, + creator: { + type: "string", + description: "Filter by issue creator username", + }, + mentioned: { + type: "string", + description: "Filter issues that mention this username", + }, + sort: { + type: "string", + enum: ["created", "updated", "comments"], + description: "Sort field (default: created)", + }, + direction: { + type: "string", + enum: ["asc", "desc"], + description: "Sort direction (default: desc)", + }, + per_page: { + type: "integer", + minimum: 1, + maximum: 100, + description: "Results per page (1-100, default: 30)", + }, + page: { + type: "integer", + minimum: 1, + description: "Page number (default: 1)", + }, + }, + required: ["owner", "repo"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const stateVal = validateEnum(params.state, ["open", "closed", "all"], "open"); + const sortVal = validateEnum(params.sort, ["created", "updated", "comments"], "created"); + const directionVal = validateEnum(params.direction, ["asc", "desc"], "desc"); + + if (!stateVal.valid) return { success: false, error: stateVal.error }; + if (!sortVal.valid) return { success: false, error: sortVal.error }; + if (!directionVal.valid) return { success: false, error: directionVal.error }; + + const perPage = clampInt(params.per_page, 1, 100, 30); + const page = clampInt(params.page, 1, 9999, 1); + + const queryParams = { + state: stateVal.value, + sort: sortVal.value, + direction: directionVal.value, + per_page: perPage, + page, + }; + if (Array.isArray(params.labels) && params.labels.length > 0) { + queryParams.labels = params.labels.join(","); + } + if (params.assignee) queryParams.assignee = params.assignee; + if (params.creator) queryParams.creator = params.creator; + if (params.mentioned) queryParams.mentioned = params.mentioned; + + const { data, pagination } = await client.getPaginated( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/issues`, + queryParams + ); + + // Filter out PRs — GitHub issues API returns both + const issues = Array.isArray(data) ? data.filter((i) => !i.pull_request) : []; + + sdk.log.info( + `github_list_issues: fetched ${issues.length} issues from ${params.owner}/${params.repo}` + ); + + const issueList = issues.map((issue) => ({ + number: issue.number, + title: issue.title, + state: issue.state, + author: issue.user?.login ?? null, + labels: issue.labels?.map((l) => (typeof l === "string" ? l : l.name)) ?? [], + assignees: issue.assignees?.map((a) => a.login) ?? [], + html_url: issue.html_url, + })); + + return { + success: true, + data: { + repo: `${params.owner}/${params.repo}`, + state: stateVal.value, + issues: issueList, + count: issues.length, + next_page: pagination.next ?? null, + }, + }; + } catch (err) { + return { success: false, error: `Failed to list issues: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_comment_issue + // ------------------------------------------------------------------------- + { + name: "github_comment_issue", + description: + "Use this when the user wants to add a comment to a GitHub issue or pull request. " + + "Returns a confirmation with the comment URL.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + issue_number: { + type: "integer", + description: "Issue or PR number to comment on (required)", + }, + body: { + type: "string", + description: "Comment text (Markdown supported, required)", + }, + }, + required: ["owner", "repo", "issue_number", "body"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "issue_number", "body"]); + if (!check.valid) return { success: false, error: check.error }; + + const issueNum = Math.floor(Number(params.issue_number)); + if (!Number.isFinite(issueNum) || issueNum < 1) { + return { success: false, error: "issue_number must be a positive integer" }; + } + + const client = createGitHubClient(sdk); + + const comment = await client.post( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/issues/${issueNum}/comments`, + { body: params.body } + ); + + sdk.log.info( + `github_comment_issue: commented on #${issueNum} in ${params.owner}/${params.repo}` + ); + + return { + success: true, + data: { + issue_number: issueNum, + repo: `${params.owner}/${params.repo}`, + comment_id: comment.id, + html_url: comment.html_url, + }, + }; + } catch (err) { + return { success: false, error: `Failed to comment on issue: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_close_issue + // ------------------------------------------------------------------------- + { + name: "github_close_issue", + description: + "Use this when the user wants to close a GitHub issue (mark as done or won't fix). " + + "Optionally posts a closing comment. Returns a confirmation.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + issue_number: { + type: "integer", + description: "Issue or PR number to close (required)", + }, + comment: { + type: "string", + description: "Optional comment to post before closing", + }, + reason: { + type: "string", + enum: ["completed", "not_planned"], + description: "Close reason: 'completed' (done) or 'not_planned' (won't fix). Default: completed", + }, + }, + required: ["owner", "repo", "issue_number"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "issue_number"]); + if (!check.valid) return { success: false, error: check.error }; + + const issueNum = Math.floor(Number(params.issue_number)); + if (!Number.isFinite(issueNum) || issueNum < 1) { + return { success: false, error: "issue_number must be a positive integer" }; + } + + const reasonVal = validateEnum( + params.reason, + ["completed", "not_planned"], + "completed" + ); + if (!reasonVal.valid) return { success: false, error: reasonVal.error }; + + const client = createGitHubClient(sdk); + const owner = encodeURIComponent(params.owner); + const repo = encodeURIComponent(params.repo); + + // Post closing comment first if provided + if (params.comment) { + await client.post(`/repos/${owner}/${repo}/issues/${issueNum}/comments`, { + body: params.comment, + }); + } + + // Close the issue + const issue = await client.patch( + `/repos/${owner}/${repo}/issues/${issueNum}`, + { + state: "closed", + state_reason: reasonVal.value, + } + ); + + sdk.log.info( + `github_close_issue: closed #${issueNum} in ${params.owner}/${params.repo} (${reasonVal.value})` + ); + + return { + success: true, + data: { + number: issueNum, + title: issue.title, + html_url: issue.html_url, + state: "closed", + reason: reasonVal.value, + }, + }; + } catch (err) { + return { success: false, error: `Failed to close issue: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_trigger_workflow + // ------------------------------------------------------------------------- + { + name: "github_trigger_workflow", + description: + "Use this when the user wants to manually trigger a GitHub Actions workflow (CI/CD pipeline). " + + "The workflow must have workflow_dispatch configured. Returns a confirmation.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + workflow_id: { + type: "string", + description: + "Workflow file name (e.g. 'ci.yml') or numeric workflow ID (required)", + }, + ref: { + type: "string", + description: "Branch or tag to run the workflow on (required, e.g. 'main')", + }, + inputs: { + type: "object", + description: "Workflow input parameters (key-value pairs, optional)", + additionalProperties: { type: "string" }, + }, + }, + required: ["owner", "repo", "workflow_id", "ref"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "workflow_id", "ref"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + const owner = encodeURIComponent(params.owner); + const repo = encodeURIComponent(params.repo); + const workflowId = encodeURIComponent(params.workflow_id); + + const body = { ref: params.ref }; + if (params.inputs && typeof params.inputs === "object") { + body.inputs = params.inputs; + } + + // POST to /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches + // Returns 204 No Content on success + const { status } = await client.postRaw( + `/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`, + body + ); + + if (status !== 204) { + return { + success: false, + error: `Unexpected response from GitHub (HTTP ${status}).`, + }; + } + + sdk.log.info( + `github_trigger_workflow: triggered ${params.workflow_id} on ${params.ref} in ${params.owner}/${params.repo}` + ); + + return { + success: true, + data: { + workflow_id: params.workflow_id, + ref: params.ref, + repo: `${params.owner}/${params.repo}`, + inputs: params.inputs ?? {}, + }, + }; + } catch (err) { + return { success: false, error: `Failed to trigger workflow: ${formatError(err)}` }; + } + }, + }, + ]; +} diff --git a/plugins/github-dev-assistant/lib/pr-manager.js b/plugins/github-dev-assistant/lib/pr-manager.js new file mode 100644 index 0000000..1e881c3 --- /dev/null +++ b/plugins/github-dev-assistant/lib/pr-manager.js @@ -0,0 +1,373 @@ +/** + * Pull request management for the github-dev-assistant plugin. + * + * Covers: + * - github_create_pr — create a new pull request + * - github_list_prs — list pull requests with filtering + * - github_merge_pr — merge a pull request (with require_pr_review check) + * + * All tools create a fresh GitHub client per execution to pick up the latest + * token from sdk.secrets (avoids stale client issues). + * + * All tools return { success, data?, error? } per the SDK ToolResult contract. + */ + +import { createGitHubClient } from "./github-client.js"; +import { validateRequired, validateEnum, clampInt, formatError } from "./utils.js"; + +/** + * Build pull request management tools. + * + * @param {object} sdk - Teleton plugin SDK (for config, logging, confirm) + * @returns {object[]} Array of tool definitions + */ +export function buildPRManagerTools(sdk) { + return [ + // ------------------------------------------------------------------------- + // Tool: github_create_pr + // ------------------------------------------------------------------------- + { + name: "github_create_pr", + description: + "Use this when the user wants to create a pull request on GitHub. " + + "Returns the PR number and URL.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + title: { + type: "string", + description: "Pull request title (required)", + }, + body: { + type: "string", + description: "Pull request description (Markdown supported)", + }, + head: { + type: "string", + description: + "Source branch name (required). For cross-repo PRs use 'owner:branch' format.", + }, + base: { + type: "string", + description: "Target/base branch name (default: repo default branch, usually 'main')", + }, + draft: { + type: "boolean", + description: "Create as draft pull request (default: false)", + }, + maintainer_can_modify: { + type: "boolean", + description: "Allow maintainers to push to the head branch (default: true)", + }, + }, + required: ["owner", "repo", "title", "head"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "title", "head"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const base = + params.base ?? + sdk.pluginConfig?.default_branch ?? + "main"; + + const body = { + title: params.title, + head: params.head, + base, + draft: params.draft ?? false, + maintainer_can_modify: params.maintainer_can_modify ?? true, + }; + if (params.body) body.body = params.body; + + const pr = await client.post( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/pulls`, + body + ); + + sdk.log.info( + `github_create_pr: created PR #${pr.number} in ${params.owner}/${params.repo}` + ); + + return { + success: true, + data: { + number: pr.number, + title: pr.title, + html_url: pr.html_url, + draft: pr.draft ?? false, + head: params.head, + base, + }, + }; + } catch (err) { + return { success: false, error: `Failed to create pull request: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_list_prs + // ------------------------------------------------------------------------- + { + name: "github_list_prs", + description: + "Use this when the user wants to see pull requests in a GitHub repository. " + + "Returns a formatted list of PRs with title, author, state, and URL.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + state: { + type: "string", + enum: ["open", "closed", "all"], + description: "Filter by state (default: open)", + }, + head: { + type: "string", + description: + "Filter by head branch (use 'user:branch' format for cross-repo)", + }, + base: { + type: "string", + description: "Filter by base branch", + }, + sort: { + type: "string", + enum: ["created", "updated", "popularity", "long-running"], + description: "Sort field (default: created)", + }, + direction: { + type: "string", + enum: ["asc", "desc"], + description: "Sort direction (default: desc)", + }, + per_page: { + type: "integer", + minimum: 1, + maximum: 100, + description: "Results per page (1-100, default: 30)", + }, + page: { + type: "integer", + minimum: 1, + description: "Page number (default: 1)", + }, + }, + required: ["owner", "repo"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const stateVal = validateEnum(params.state, ["open", "closed", "all"], "open"); + const sortVal = validateEnum( + params.sort, + ["created", "updated", "popularity", "long-running"], + "created" + ); + const directionVal = validateEnum(params.direction, ["asc", "desc"], "desc"); + + if (!stateVal.valid) return { success: false, error: stateVal.error }; + if (!sortVal.valid) return { success: false, error: sortVal.error }; + if (!directionVal.valid) return { success: false, error: directionVal.error }; + + const perPage = clampInt(params.per_page, 1, 100, 30); + const page = clampInt(params.page, 1, 9999, 1); + + const queryParams = { + state: stateVal.value, + sort: sortVal.value, + direction: directionVal.value, + per_page: perPage, + page, + }; + if (params.head) queryParams.head = params.head; + if (params.base) queryParams.base = params.base; + + const { data, pagination } = await client.getPaginated( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/pulls`, + queryParams + ); + + const prs = Array.isArray(data) ? data : []; + + sdk.log.info(`github_list_prs: fetched ${prs.length} PRs from ${params.owner}/${params.repo}`); + + const prList = prs.map((pr) => ({ + number: pr.number, + title: pr.title, + state: pr.state, + draft: pr.draft ?? false, + author: pr.user?.login ?? null, + labels: pr.labels?.map((l) => l.name) ?? [], + html_url: pr.html_url, + })); + + return { + success: true, + data: { + repo: `${params.owner}/${params.repo}`, + state: stateVal.value, + prs: prList, + count: prs.length, + next_page: pagination.next ?? null, + }, + }; + } catch (err) { + return { success: false, error: `Failed to list pull requests: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_merge_pr + // ------------------------------------------------------------------------- + { + name: "github_merge_pr", + description: + "Use this when the user wants to merge a pull request on GitHub. " + + "Returns confirmation of the merge with the commit SHA. " + + "When require_pr_review config is true, explicitly ask the user to confirm before calling this tool.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + pr_number: { + type: "integer", + description: "Pull request number to merge (required)", + }, + merge_method: { + type: "string", + enum: ["merge", "squash", "rebase"], + description: "Merge strategy (default: merge)", + }, + commit_title: { + type: "string", + description: "Custom commit title for merge/squash commits", + }, + commit_message: { + type: "string", + description: "Custom commit message body for merge/squash commits", + }, + confirmed: { + type: "boolean", + description: + "Set to true when the user has explicitly confirmed they want to merge. " + + "Required when require_pr_review config option is enabled.", + }, + }, + required: ["owner", "repo", "pr_number"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "pr_number"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const prNum = Math.floor(Number(params.pr_number)); + if (!Number.isFinite(prNum) || prNum < 1) { + return { success: false, error: "pr_number must be a positive integer" }; + } + + const mergeMethodVal = validateEnum( + params.merge_method, + ["merge", "squash", "rebase"], + "merge" + ); + if (!mergeMethodVal.valid) return { success: false, error: mergeMethodVal.error }; + + // Security policy: check require_pr_review + // Since sdk.llm.confirm() does not exist in the SDK, the confirmation + // is handled by the LLM itself — it should ask the user before calling + // this tool when require_pr_review is enabled. The `confirmed` param + // signals that the user has explicitly approved the merge. + const requireReview = sdk.pluginConfig?.require_pr_review ?? false; + const confirmed = params.confirmed ?? false; + + if (requireReview && !confirmed) { + // Fetch PR details so the LLM can surface them in the confirmation prompt + let prTitle = `PR #${prNum}`; + try { + const prData = await client.get( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/pulls/${prNum}` + ); + prTitle = `PR #${prNum}: ${prData.title}`; + } catch { + // Non-fatal — use generic title + } + + return { + success: false, + error: + `The require_pr_review policy is enabled. Please ask the user to explicitly confirm ` + + `they want to merge ${prTitle} in ${params.owner}/${params.repo} ` + + `using the ${mergeMethodVal.value} strategy, ` + + `then call this tool again with confirmed=true.`, + }; + } + + const body = { + merge_method: mergeMethodVal.value, + }; + if (params.commit_title) body.commit_title = params.commit_title; + if (params.commit_message) body.commit_message = params.commit_message; + + const result = await client.put( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/pulls/${prNum}/merge`, + body + ); + + sdk.log.info( + `github_merge_pr: merged PR #${prNum} in ${params.owner}/${params.repo} via ${mergeMethodVal.value}` + ); + + return { + success: true, + data: { + pr_number: prNum, + repo: `${params.owner}/${params.repo}`, + merge_method: mergeMethodVal.value, + sha: result.sha ?? null, + message: result.message ?? "Merged successfully.", + }, + }; + } catch (err) { + return { success: false, error: `Failed to merge pull request: ${formatError(err)}` }; + } + }, + }, + ]; +} diff --git a/plugins/github-dev-assistant/lib/repo-ops.js b/plugins/github-dev-assistant/lib/repo-ops.js new file mode 100644 index 0000000..bec1676 --- /dev/null +++ b/plugins/github-dev-assistant/lib/repo-ops.js @@ -0,0 +1,503 @@ +/** + * Repository, file, and branch operations for the github-dev-assistant plugin. + * + * Covers: + * - github_list_repos — list user/org repositories + * - github_create_repo — create a new repository + * - github_get_file — read file or directory content + * - github_update_file — create or update a file with a commit + * - github_create_branch — create a new branch from a ref + * + * All tools create a fresh GitHub client per execution to pick up the latest + * token from sdk.secrets (avoids stale client issues). + * + * All tools return { success, data?, error? } per the SDK ToolResult contract. + */ + +import { createGitHubClient } from "./github-client.js"; +import { decodeBase64, encodeBase64, validateRequired, validateEnum, clampInt, formatError } from "./utils.js"; + +/** + * Build repository operations tools. + * + * @param {object} sdk - Teleton plugin SDK (for config, logging, secrets) + * @returns {object[]} Array of tool definitions + */ +export function buildRepoOpsTools(sdk) { + // Resolve owner from params, falling back to plugin config, then authenticated user + async function resolveOwner(client, owner) { + if (owner) return owner; + const configOwner = sdk.pluginConfig?.default_owner ?? null; + if (configOwner) return configOwner; + // Fall back to the authenticated user's login + const user = await client.get("/user"); + return user.login; + } + + return [ + // ------------------------------------------------------------------------- + // Tool: github_list_repos + // ------------------------------------------------------------------------- + { + name: "github_list_repos", + description: + "Use this when the user wants to see their GitHub repositories or a list of repos for a user/org. " + + "Returns a formatted list of repositories with name, description, language, and visibility.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: + "GitHub username or organization name. Omit to use the authenticated user.", + }, + type: { + type: "string", + enum: ["all", "owner", "public", "private", "forks", "sources", "member"], + description: "Filter by repository type (default: all)", + }, + sort: { + type: "string", + enum: ["created", "updated", "pushed", "full_name"], + description: "Sort field (default: full_name)", + }, + direction: { + type: "string", + enum: ["asc", "desc"], + description: "Sort direction (default: asc for full_name, desc otherwise)", + }, + per_page: { + type: "integer", + minimum: 1, + maximum: 100, + description: "Results per page (1-100, default: 30)", + }, + page: { + type: "integer", + minimum: 1, + description: "Page number (default: 1)", + }, + }, + }, + execute: async (params, _context) => { + try { + const client = createGitHubClient(sdk); + const owner = await resolveOwner(client, params.owner ?? null); + const perPage = clampInt(params.per_page, 1, 100, 30); + const page = clampInt(params.page, 1, 9999, 1); + + const typeVal = validateEnum( + params.type, + ["all", "owner", "public", "private", "forks", "sources", "member"], + "all" + ); + const sortVal = validateEnum( + params.sort, + ["created", "updated", "pushed", "full_name"], + "full_name" + ); + const directionVal = validateEnum( + params.direction, + ["asc", "desc"], + "asc" + ); + + if (!typeVal.valid) return { success: false, error: typeVal.error }; + if (!sortVal.valid) return { success: false, error: sortVal.error }; + if (!directionVal.valid) return { success: false, error: directionVal.error }; + + // Determine endpoint: /user/repos for self, /users/:owner/repos or /orgs/:owner/repos + let path; + if (!params.owner) { + path = "/user/repos"; + } else { + path = `/users/${encodeURIComponent(owner)}/repos`; + } + + const { data, pagination } = await client.getPaginated(path, { + type: typeVal.value, + sort: sortVal.value, + direction: directionVal.value, + per_page: perPage, + page, + }); + + const repos = Array.isArray(data) ? data : []; + + sdk.log.info(`github_list_repos: fetched ${repos.length} repos for ${owner}`); + + if (repos.length === 0) { + return { success: true, data: { owner, repos: [], message: `No repositories found for ${owner}.` } }; + } + + const repoList = repos.map((r) => ({ + name: r.name, + full_name: r.full_name, + description: r.description ?? null, + language: r.language ?? null, + private: r.private, + html_url: r.html_url, + })); + + return { + success: true, + data: { + owner, + repos: repoList, + count: repos.length, + next_page: pagination.next ?? null, + }, + }; + } catch (err) { + return { success: false, error: `Failed to list repositories: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_create_repo + // ------------------------------------------------------------------------- + { + name: "github_create_repo", + description: + "Use this when the user wants to create a new GitHub repository. " + + "Returns the URL of the newly created repository.", + category: "action", + parameters: { + type: "object", + properties: { + name: { + type: "string", + description: "Repository name (required, lowercase letters, numbers, hyphens)", + }, + description: { + type: "string", + description: "Short description of the repository", + }, + private: { + type: "boolean", + description: "Create as private repository (default: false)", + }, + auto_init: { + type: "boolean", + description: "Auto-initialize with a README (default: false)", + }, + license_template: { + type: "string", + enum: ["mit", "apache-2.0", "gpl-3.0", "bsd-2-clause", "bsd-3-clause", "mpl-2.0", "lgpl-3.0", "agpl-3.0", "unlicense"], + description: "License template to apply (optional)", + }, + gitignore_template: { + type: "string", + description: "Gitignore template to use, e.g. 'Node', 'Python' (optional)", + }, + }, + required: ["name"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["name"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const body = { + name: params.name, + private: params.private ?? false, + auto_init: params.auto_init ?? false, + }; + if (params.description) body.description = params.description; + if (params.license_template) body.license_template = params.license_template; + if (params.gitignore_template) body.gitignore_template = params.gitignore_template; + + const repo = await client.post("/user/repos", body); + + sdk.log.info(`github_create_repo: created ${repo.full_name}`); + + return { + success: true, + data: { + full_name: repo.full_name, + html_url: repo.html_url, + private: repo.private, + message: `Repository ${repo.full_name} created successfully.`, + }, + }; + } catch (err) { + return { success: false, error: `Failed to create repository: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_get_file + // ------------------------------------------------------------------------- + { + name: "github_get_file", + description: + "Use this when the user wants to read a file or list a directory from a GitHub repository. " + + "Returns the file content as text, or lists directory entries.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or org)", + }, + repo: { + type: "string", + description: "Repository name", + }, + path: { + type: "string", + description: "Path to file or directory within the repo (e.g. 'src/index.js')", + }, + ref: { + type: "string", + description: "Branch, tag, or commit SHA to read from (default: repo default branch)", + }, + }, + required: ["owner", "repo", "path"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "path"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + const queryParams = {}; + if (params.ref) queryParams.ref = params.ref; + + const data = await client.get( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/contents/${params.path}`, + queryParams + ); + + // Directory listing + if (Array.isArray(data)) { + const entries = data.map((e) => ({ + name: e.name, + path: e.path, + type: e.type, + size: e.size ?? 0, + sha: e.sha, + })); + return { + success: true, + data: { + type: "directory", + path: params.path, + repo: `${params.owner}/${params.repo}`, + entries, + }, + }; + } + + // Single file + const content = data.content ? decodeBase64(data.content) : null; + + sdk.log.info(`github_get_file: read ${data.path} (${data.size} bytes)`); + + return { + success: true, + data: { + type: "file", + path: data.path, + repo: `${params.owner}/${params.repo}`, + size: data.size, + sha: data.sha, + content: content ?? null, + encoding: content ? "utf8" : null, + html_url: data.html_url, + }, + }; + } catch (err) { + return { success: false, error: `Failed to get file: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_update_file + // ------------------------------------------------------------------------- + { + name: "github_update_file", + description: + "Use this when the user wants to create a new file or update an existing file in a GitHub repository. " + + "For updates, first call github_get_file to get the current SHA. " + + "Returns the commit URL on success.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + path: { + type: "string", + description: "Path to the file within the repo (e.g. 'src/index.js')", + }, + content: { + type: "string", + description: "UTF-8 text content to write to the file", + }, + message: { + type: "string", + description: "Commit message", + }, + branch: { + type: "string", + description: "Branch to commit to (defaults to the repo's default branch)", + }, + sha: { + type: "string", + description: "Current file SHA — required when updating an existing file, omit for new files", + }, + committer_name: { + type: "string", + description: "Committer name (defaults to plugin config commit_author_name)", + }, + committer_email: { + type: "string", + description: "Committer email (defaults to plugin config commit_author_email)", + }, + }, + required: ["owner", "repo", "path", "content", "message"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "path", "content", "message"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + + const authorName = + params.committer_name ?? + sdk.pluginConfig?.commit_author_name ?? + "Teleton AI Agent"; + const authorEmail = + params.committer_email ?? + sdk.pluginConfig?.commit_author_email ?? + "agent@teleton.local"; + + const body = { + message: params.message, + content: encodeBase64(params.content), + committer: { name: authorName, email: authorEmail }, + }; + + if (params.branch) body.branch = params.branch; + if (params.sha) body.sha = params.sha; + + const result = await client.put( + `/repos/${encodeURIComponent(params.owner)}/${encodeURIComponent(params.repo)}/contents/${params.path}`, + body + ); + + sdk.log.info( + `github_update_file: committed ${params.path} to ${params.owner}/${params.repo}` + ); + + const action = params.sha ? "updated" : "created"; + return { + success: true, + data: { + action, + path: params.path, + repo: `${params.owner}/${params.repo}`, + commit_sha: result.commit?.sha ?? null, + commit_url: result.commit?.html_url ?? null, + message: params.message, + }, + }; + } catch (err) { + return { success: false, error: `Failed to update file: ${formatError(err)}` }; + } + }, + }, + + // ------------------------------------------------------------------------- + // Tool: github_create_branch + // ------------------------------------------------------------------------- + { + name: "github_create_branch", + description: + "Use this when the user wants to create a new branch in a GitHub repository. " + + "Returns the new branch name and its starting commit SHA.", + category: "action", + parameters: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner", + }, + repo: { + type: "string", + description: "Repository name", + }, + branch: { + type: "string", + description: "Name for the new branch", + }, + from_ref: { + type: "string", + description: "Source branch, tag, or commit SHA to branch from (default: repo default branch)", + }, + }, + required: ["owner", "repo", "branch"], + }, + execute: async (params, _context) => { + try { + const check = validateRequired(params, ["owner", "repo", "branch"]); + if (!check.valid) return { success: false, error: check.error }; + + const client = createGitHubClient(sdk); + const owner = encodeURIComponent(params.owner); + const repo = encodeURIComponent(params.repo); + + // Resolve the SHA of the source ref + const fromRef = params.from_ref ?? sdk.pluginConfig?.default_branch ?? "main"; + const refData = await client.get(`/repos/${owner}/${repo}/git/ref/heads/${encodeURIComponent(fromRef)}`); + const sha = refData.object?.sha; + if (!sha) { + return { + success: false, + error: `Failed to create branch: could not resolve source branch "${fromRef}".`, + }; + } + + // Create the new branch ref + const result = await client.post(`/repos/${owner}/${repo}/git/refs`, { + ref: `refs/heads/${params.branch}`, + sha, + }); + + sdk.log.info( + `github_create_branch: created ${params.branch} from ${fromRef} in ${params.owner}/${params.repo}` + ); + + const newSha = result.object?.sha ?? sha; + return { + success: true, + data: { + branch: params.branch, + repo: `${params.owner}/${params.repo}`, + from_ref: fromRef, + sha: newSha, + }, + }; + } catch (err) { + return { success: false, error: `Failed to create branch: ${formatError(err)}` }; + } + }, + }, + ]; +} diff --git a/plugins/github-dev-assistant/lib/utils.js b/plugins/github-dev-assistant/lib/utils.js new file mode 100644 index 0000000..2e37334 --- /dev/null +++ b/plugins/github-dev-assistant/lib/utils.js @@ -0,0 +1,173 @@ +/** + * Utility helpers for the github-dev-assistant plugin. + * + * Contains: input validation, base64 encoding/decoding, error formatting, + * pagination helpers, and rate-limit tracking. + */ + +import { randomBytes } from "node:crypto"; + +// --------------------------------------------------------------------------- +// Cryptographic helpers +// --------------------------------------------------------------------------- + +/** + * Generate a cryptographically random state token for OAuth CSRF protection. + * @param {number} [bytes=32] - Number of random bytes (hex-encoded, so output is 2x longer) + * @returns {string} Hex-encoded random string + */ +export function generateState(bytes = 32) { + return randomBytes(bytes).toString("hex"); +} + +// --------------------------------------------------------------------------- +// Base64 helpers (for GitHub Content API) +// --------------------------------------------------------------------------- + +/** + * Decode a base64 string (possibly with line breaks) to UTF-8 text. + * GitHub's Content API returns base64 content with newlines every 60 chars. + * @param {string} b64 - Base64-encoded string + * @returns {string} Decoded UTF-8 string + */ +export function decodeBase64(b64) { + // Remove any whitespace/newlines that GitHub inserts + const clean = b64.replace(/\s/g, ""); + return Buffer.from(clean, "base64").toString("utf8"); +} + +/** + * Encode a UTF-8 string to base64 for GitHub Content API uploads. + * @param {string} text - UTF-8 string + * @returns {string} Base64-encoded string + */ +export function encodeBase64(text) { + return Buffer.from(text, "utf8").toString("base64"); +} + +// --------------------------------------------------------------------------- +// Input validation helpers +// --------------------------------------------------------------------------- + +/** + * Validate that required string parameters are present and non-empty. + * @param {object} params - Parameter object from tool execute() + * @param {string[]} required - List of required parameter names + * @returns {{ valid: boolean, error?: string }} + */ +export function validateRequired(params, required) { + for (const key of required) { + if (params[key] === undefined || params[key] === null || params[key] === "") { + return { valid: false, error: `Missing required parameter: ${key}` }; + } + } + return { valid: true }; +} + +/** + * Clamp an integer parameter to a safe range. + * @param {number|undefined} value + * @param {number} min + * @param {number} max + * @param {number} defaultValue + * @returns {number} + */ +export function clampInt(value, min, max, defaultValue) { + if (value === undefined || value === null) return defaultValue; + const n = Math.floor(Number(value)); + if (isNaN(n)) return defaultValue; + return Math.max(min, Math.min(max, n)); +} + +/** + * Validate an enum value against an allowed list. + * @param {string|undefined} value + * @param {string[]} allowed + * @param {string} defaultValue + * @returns {{ valid: boolean, value: string, error?: string }} + */ +export function validateEnum(value, allowed, defaultValue) { + if (value === undefined || value === null) { + return { valid: true, value: defaultValue }; + } + if (!allowed.includes(value)) { + return { + valid: false, + value: defaultValue, + error: `Invalid value "${value}". Allowed: ${allowed.join(", ")}`, + }; + } + return { valid: true, value }; +} + +// --------------------------------------------------------------------------- +// Error formatting +// --------------------------------------------------------------------------- + +/** + * Format a caught error into a clean error message string. + * Never exposes internal file paths or token fragments. + * @param {unknown} err + * @param {string} [fallback] + * @returns {string} + */ +export function formatError(err, fallback = "An unexpected error occurred") { + if (!err) return fallback; + const msg = String(err?.message ?? err); + // Redact anything that looks like a token or secret + return msg + .replace(/ghp_[A-Za-z0-9]+/g, "[REDACTED]") + .replace(/ghs_[A-Za-z0-9]+/g, "[REDACTED]") + .replace(/ghu_[A-Za-z0-9]+/g, "[REDACTED]") + .replace(/Bearer [A-Za-z0-9\-._~+/]+=*/g, "Bearer [REDACTED]") + .slice(0, 500); +} + +// --------------------------------------------------------------------------- +// Rate limiting (simple token-bucket per instance) +// --------------------------------------------------------------------------- + +/** + * Create a simple rate-limiter that enforces a minimum delay between calls. + * @param {number} minDelayMs - Minimum milliseconds between calls + * @returns {{ wait: () => Promise }} + */ +export function createRateLimiter(minDelayMs) { + let lastCallTime = 0; + return { + async wait() { + const elapsed = Date.now() - lastCallTime; + if (elapsed < minDelayMs) { + await new Promise((r) => setTimeout(r, minDelayMs - elapsed)); + } + lastCallTime = Date.now(); + }, + }; +} + +// --------------------------------------------------------------------------- +// Pagination helpers +// --------------------------------------------------------------------------- + +/** + * Extract pagination info from GitHub Link header. + * @param {string|null} linkHeader - Value of the Link response header + * @returns {{ next: number|null, prev: number|null, last: number|null }} + */ +export function parseLinkHeader(linkHeader) { + const result = { next: null, prev: null, last: null }; + if (!linkHeader) return result; + + const parts = linkHeader.split(",").map((p) => p.trim()); + for (const part of parts) { + const match = part.match(/<[^>]*[?&]page=(\d+)[^>]*>;\s*rel="(\w+)"/); + if (match) { + const page = parseInt(match[1], 10); + const rel = match[2]; + if (rel === "next") result.next = page; + else if (rel === "prev") result.prev = page; + else if (rel === "last") result.last = page; + } + } + return result; +} diff --git a/plugins/github-dev-assistant/manifest.json b/plugins/github-dev-assistant/manifest.json new file mode 100644 index 0000000..062e9b1 --- /dev/null +++ b/plugins/github-dev-assistant/manifest.json @@ -0,0 +1,48 @@ +{ + "id": "github-dev-assistant", + "name": "GitHub Dev Assistant", + "version": "1.0.0", + "description": "Full GitHub development workflow automation — repos, files, branches, PRs, issues, and GitHub Actions via Personal Access Token", + "author": { + "name": "xlabtg", + "url": "https://github.com/xlabtg" + }, + "license": "MIT", + "entry": "index.js", + "teleton": ">=1.0.0", + "sdkVersion": ">=1.0.0", + "secrets": { + "github_token": { + "required": true, + "env": "GITHUB_DEV_ASSISTANT_GITHUB_TOKEN", + "description": "GitHub Personal Access Token (create at https://github.com/settings/tokens)" + } + }, + "defaultConfig": { + "default_owner": null, + "default_branch": "main", + "require_pr_review": false, + "commit_author_name": "Teleton AI Agent", + "commit_author_email": "agent@teleton.local" + }, + "tools": [ + { "name": "github_check_auth", "description": "Check if GitHub is connected and verify the authenticated account" }, + { "name": "github_list_repos", "description": "List GitHub repositories for a user or organization" }, + { "name": "github_create_repo", "description": "Create a new GitHub repository" }, + { "name": "github_get_file", "description": "Read a file or list a directory from a GitHub repository" }, + { "name": "github_update_file", "description": "Create or update a file in a GitHub repository with a commit" }, + { "name": "github_create_branch", "description": "Create a new branch in a GitHub repository" }, + { "name": "github_create_pr", "description": "Create a new pull request" }, + { "name": "github_list_prs", "description": "List pull requests in a repository" }, + { "name": "github_merge_pr", "description": "Merge a pull request" }, + { "name": "github_create_issue", "description": "Create a new issue in a repository" }, + { "name": "github_list_issues", "description": "List issues in a repository" }, + { "name": "github_comment_issue", "description": "Add a comment to an issue or pull request" }, + { "name": "github_close_issue", "description": "Close an issue or pull request" }, + { "name": "github_trigger_workflow", "description": "Manually trigger a GitHub Actions workflow" } + ], + "permissions": [], + "tags": ["github", "development", "automation", "git", "ci-cd"], + "repository": "https://github.com/xlabtg/teleton-plugins", + "funding": null +} diff --git a/plugins/ton-bridge/README.md b/plugins/ton-bridge/README.md new file mode 100644 index 0000000..dbc328c --- /dev/null +++ b/plugins/ton-bridge/README.md @@ -0,0 +1,77 @@ +# TON Bridge + +Share the TON Bridge Mini App link with an inline button in Telegram chats. +Works in DMs, groups, and channels. + +TON Bridge works with support from TONBANKCARD. + +## Tools + +| Tool | Description | Category | +|------|-------------|----------| +| `ton_bridge_open` | Send a message with a TON Bridge Mini App button | action | +| `ton_bridge_about` | Send info about TON Bridge with a Mini App button | data-bearing | +| `ton_bridge_custom_message` | Send a custom message alongside a TON Bridge button | action | + +## Install + +```bash +mkdir -p ~/.teleton/plugins +cp -r plugins/ton-bridge ~/.teleton/plugins/ +``` + +Restart Teleton — the plugin is auto-loaded from `~/.teleton/plugins/`. No changes to `config.yaml` are required. + +## Usage examples + +- "Open TON Bridge" +- "Tell me about TON Bridge" +- "Send a message about TON Bridge with a button" +- "Open TON Bridge, no emoji on the button" +- "Share a TON Bridge link with the text: Transfer your assets seamlessly" + +## Configuration + +Configuration is optional — the plugin works out of the box with defaults. Override in `config.yaml` only if needed: + +```yaml +# ~/.teleton/config.yaml +plugins: + ton_bridge: + buttonText: "TON Bridge No1" # Default button label (default: "TON Bridge No1") + startParam: "" # Optional start parameter appended to the Mini App URL +``` + +> **Note:** Button emoji is controlled by the agent at call time via the `buttonText` parameter, not by config. This allows the agent to include or omit emoji as requested by the user. + +## Tool schemas + +### `ton_bridge_open` + +Send a message with a TON Bridge Mini App button. Use when the user asks to open or access TON Bridge. + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `message` | string | No | — | Optional message text to show with the button | +| `buttonText` | string | No | config default | Button label. Do not include emoji unless user requested it. | + +### `ton_bridge_about` + +Send an info message about TON Bridge with a Mini App button. Use when the user asks about TON Bridge. + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `buttonText` | string | No | config default | Button label. Do not include emoji unless user requested it. | + +### `ton_bridge_custom_message` + +Send a custom message alongside a TON Bridge button. + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `customMessage` | string | Yes | — | Custom message text to display with the button | +| `buttonText` | string | No | config default | Button label. Do not include emoji unless user requested it. | + +--- + +**Developer:** [xlabtg](https://github.com/xlabtg) diff --git a/plugins/ton-bridge/index.js b/plugins/ton-bridge/index.js new file mode 100644 index 0000000..1d3f672 --- /dev/null +++ b/plugins/ton-bridge/index.js @@ -0,0 +1,205 @@ +/** + * TON Bridge plugin + * + * Provides LLM-callable tools to share the TON Bridge Mini App link. + * Pattern B (SDK) — uses sdk.pluginConfig, sdk.log, sdk.telegram.sendMessage + * + * Actively sends messages with URL inline buttons so the button renders + * correctly in DMs, groups, and channels. + */ + +// ─── Manifest (inline) ──────────────────────────────────────────────────────── +// The runtime reads this export for sdkVersion and defaultConfig. +// The manifest.json file is used by the registry for discovery. + +export const manifest = { + name: "ton-bridge", + version: "1.0.0", + sdkVersion: ">=1.0.0", + description: "Share TON Bridge Mini App link with a button. Opens https://t.me/TONBridge_robot?startapp", + defaultConfig: { + buttonText: "TON Bridge No1", + startParam: "", + }, +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +const MINI_APP_URL = "https://t.me/TONBridge_robot?startapp"; + +function buildUrl(startParam) { + return startParam + ? `${MINI_APP_URL}=${encodeURIComponent(startParam)}` + : MINI_APP_URL; +} + +// ─── Tools ──────────────────────────────────────────────────────────────────── + +export const tools = (sdk) => [ + // ── Tool: ton_bridge_open ───────────────────────────────────────────────── + { + name: "ton_bridge_open", + description: + "Send a message with a TON Bridge Mini App button. Use when the user asks to open or access TON Bridge. Sends the message directly to the current chat.", + category: "action", + parameters: { + type: "object", + properties: { + message: { + type: "string", + description: "Optional message text to show with the button", + minLength: 1, + maxLength: 500, + }, + buttonText: { + type: "string", + description: "Button label text. Omit to use the configured default. Do NOT include emoji here unless the user explicitly requested one.", + minLength: 1, + maxLength: 64, + }, + }, + }, + execute: async (params, context) => { + try { + const buttonText = params.buttonText ?? sdk.pluginConfig?.buttonText ?? "TON Bridge No1"; + const startParam = sdk.pluginConfig?.startParam ?? ""; + const url = buildUrl(startParam); + + const text = + params.message ?? + "TON Bridge — The #1 Bridge in the TON Catalog\n\nClick the button below to open TON Bridge Mini App."; + + sdk.log?.info( + `ton_bridge_open called by ${context?.senderId ?? "unknown"}` + ); + + const messageId = await sdk.telegram.sendMessage( + context.chatId, + text, + { + inlineKeyboard: [[{ text: buttonText, url }]], + } + ); + + return { + success: true, + data: { message_id: messageId, chat_id: context.chatId }, + }; + } catch (err) { + if (err.name === "PluginSDKError") { + sdk.log?.error(`ton_bridge_open failed: ${err.code}: ${err.message}`); + return { success: false, error: `${err.code}: ${String(err.message).slice(0, 500)}` }; + } + sdk.log?.error("ton_bridge_open failed:", err.message); + return { success: false, error: String(err.message || err).slice(0, 500) }; + } + }, + }, + + // ── Tool: ton_bridge_about ──────────────────────────────────────────────── + { + name: "ton_bridge_about", + description: + "Send an info message about TON Bridge with a Mini App button. Use when the user asks about TON Bridge or wants more information.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + buttonText: { + type: "string", + description: "Button label text. Omit to use the configured default. Do NOT include emoji here unless the user explicitly requested one.", + minLength: 1, + maxLength: 64, + }, + }, + }, + execute: async (params, context) => { + try { + const buttonText = params.buttonText ?? sdk.pluginConfig?.buttonText ?? "TON Bridge No1"; + const startParam = sdk.pluginConfig?.startParam ?? ""; + const url = buildUrl(startParam); + + sdk.log?.info( + `ton_bridge_about called by ${context?.senderId ?? "unknown"}` + ); + + const messageId = await sdk.telegram.sendMessage( + context.chatId, + "About TON Bridge\n\nTON Bridge is the #1 bridge in the TON Catalog. Transfer assets across chains seamlessly via the official Mini App.", + { + inlineKeyboard: [[{ text: buttonText, url }]], + } + ); + + return { + success: true, + data: { message_id: messageId, chat_id: context.chatId }, + }; + } catch (err) { + if (err.name === "PluginSDKError") { + sdk.log?.error(`ton_bridge_about failed: ${err.code}: ${err.message}`); + return { success: false, error: `${err.code}: ${String(err.message).slice(0, 500)}` }; + } + sdk.log?.error("ton_bridge_about failed:", err.message); + return { success: false, error: String(err.message || err).slice(0, 500) }; + } + }, + }, + + // ── Tool: ton_bridge_custom_message ────────────────────────────────────── + { + name: "ton_bridge_custom_message", + description: + "Send a custom message alongside a TON Bridge button. Use when the user wants to share a specific message with the TON Bridge link.", + category: "action", + parameters: { + type: "object", + properties: { + customMessage: { + type: "string", + description: "Custom message text to display with the button", + minLength: 1, + maxLength: 500, + }, + buttonText: { + type: "string", + description: "Button label text. Omit to use the configured default. Do NOT include emoji here unless the user explicitly requested one.", + minLength: 1, + maxLength: 64, + }, + }, + required: ["customMessage"], + }, + execute: async (params, context) => { + try { + const buttonText = params.buttonText ?? sdk.pluginConfig?.buttonText ?? "TON Bridge No1"; + const startParam = sdk.pluginConfig?.startParam ?? ""; + const url = buildUrl(startParam); + + sdk.log?.info( + `ton_bridge_custom_message called by ${context?.senderId ?? "unknown"}` + ); + + const messageId = await sdk.telegram.sendMessage( + context.chatId, + params.customMessage, + { + inlineKeyboard: [[{ text: buttonText, url }]], + } + ); + + return { + success: true, + data: { message_id: messageId, chat_id: context.chatId }, + }; + } catch (err) { + if (err.name === "PluginSDKError") { + sdk.log?.error(`ton_bridge_custom_message failed: ${err.code}: ${err.message}`); + return { success: false, error: `${err.code}: ${String(err.message).slice(0, 500)}` }; + } + sdk.log?.error("ton_bridge_custom_message failed:", err.message); + return { success: false, error: String(err.message || err).slice(0, 500) }; + } + }, + }, +]; diff --git a/plugins/ton-bridge/manifest.json b/plugins/ton-bridge/manifest.json new file mode 100644 index 0000000..5631d91 --- /dev/null +++ b/plugins/ton-bridge/manifest.json @@ -0,0 +1,27 @@ +{ + "id": "ton-bridge", + "name": "TON Bridge", + "version": "1.0.0", + "description": "Share TON Bridge Mini App link with a button. Opens https://t.me/TONBridge_robot?startapp", + "author": { + "name": "xlabtg", + "url": "https://github.com/xlabtg" + }, + "license": "MIT", + "entry": "index.js", + "teleton": ">=1.0.0", + "sdkVersion": ">=1.0.0", + "tools": [ + { "name": "ton_bridge_open", "description": "Send a message with a TON Bridge Mini App link" }, + { "name": "ton_bridge_about", "description": "Send info about TON Bridge with a link to the Mini App" }, + { "name": "ton_bridge_custom_message", "description": "Send a custom message alongside a TON Bridge button" } + ], + "defaultConfig": { + "buttonText": "TON Bridge No1", + "startParam": "" + }, + "permissions": [], + "tags": ["ton", "bridge", "miniapp", "tonbridge"], + "repository": "https://github.com/xlabtg/teleton-plugins", + "funding": null +} diff --git a/plugins/ton-bridge/tests/index.test.js b/plugins/ton-bridge/tests/index.test.js new file mode 100644 index 0000000..929ae40 --- /dev/null +++ b/plugins/ton-bridge/tests/index.test.js @@ -0,0 +1,333 @@ +/** + * Unit tests for ton-bridge plugin + * + * Tests manifest exports, tool definitions, and tool execute behavior + * using Node's built-in test runner (node:test). + */ + +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; +import { resolve, join } from "node:path"; + +const PLUGIN_DIR = resolve("plugins/ton-bridge"); +const PLUGIN_URL = pathToFileURL(join(PLUGIN_DIR, "index.js")).href; + +// ─── Minimal mock SDK ──────────────────────────────────────────────────────── + +function makeSdk(overrides = {}) { + return { + pluginConfig: { + buttonText: "TON Bridge No1", + startParam: "", + }, + log: { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }, + telegram: { + sendMessage: async () => 42, + ...overrides.telegram, + }, + ...overrides, + }; +} + +function makeContext(overrides = {}) { + return { + chatId: 123456789, + senderId: 987654321, + ...overrides, + }; +} + +// ─── Load plugin once ───────────────────────────────────────────────────────── + +let mod; + +before(async () => { + mod = await import(PLUGIN_URL); +}); + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe("ton-bridge plugin", () => { + describe("manifest", () => { + it("exports manifest object", () => { + assert.ok(mod.manifest, "manifest should be exported"); + assert.equal(typeof mod.manifest, "object"); + }); + + it("manifest has required name field", () => { + assert.equal(mod.manifest.name, "ton-bridge"); + }); + + it("manifest has version", () => { + assert.ok(mod.manifest.version, "manifest.version should exist"); + }); + + it("manifest has sdkVersion", () => { + assert.ok(mod.manifest.sdkVersion, "manifest.sdkVersion should exist"); + }); + + it("manifest has defaultConfig with buttonText", () => { + assert.ok(mod.manifest.defaultConfig, "defaultConfig should exist"); + assert.ok(mod.manifest.defaultConfig.buttonText, "defaultConfig.buttonText should exist"); + }); + }); + + describe("tools export", () => { + it("exports tools as a function", () => { + assert.equal(typeof mod.tools, "function", "tools should be a function"); + }); + + it("tools(sdk) returns an array", () => { + const sdk = makeSdk(); + const toolList = mod.tools(sdk); + assert.ok(Array.isArray(toolList), "tools(sdk) should return an array"); + }); + + it("returns 3 tools", () => { + const sdk = makeSdk(); + const toolList = mod.tools(sdk); + assert.equal(toolList.length, 3, "should have 3 tools"); + }); + + it("all tools have required fields: name, description, execute", () => { + const sdk = makeSdk(); + const toolList = mod.tools(sdk); + for (const tool of toolList) { + assert.ok(tool.name, `tool.name must exist (got: ${JSON.stringify(tool.name)})`); + assert.ok(tool.description, `tool "${tool.name}" must have description`); + assert.equal(typeof tool.execute, "function", `tool "${tool.name}" must have execute function`); + } + }); + + it("tool names match expected set", () => { + const sdk = makeSdk(); + const names = mod.tools(sdk).map((t) => t.name); + assert.ok(names.includes("ton_bridge_open"), "should have ton_bridge_open"); + assert.ok(names.includes("ton_bridge_about"), "should have ton_bridge_about"); + assert.ok(names.includes("ton_bridge_custom_message"), "should have ton_bridge_custom_message"); + }); + }); + + describe("ton_bridge_open", () => { + it("returns success when sendMessage succeeds", async () => { + let capturedChatId, capturedText, capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedChatId = chatId; + capturedText = text; + capturedOpts = opts; + return 55; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + const result = await tool.execute({}, makeContext({ chatId: 111 })); + + assert.equal(result.success, true); + assert.equal(result.data.message_id, 55); + assert.equal(result.data.chat_id, 111); + assert.equal(capturedChatId, 111); + assert.ok(capturedText, "message text should be provided"); + assert.ok(capturedOpts.inlineKeyboard, "inline keyboard should be included"); + }); + + it("uses custom message when provided", async () => { + let capturedText; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text) => { + capturedText = text; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({ message: "Custom text" }, makeContext()); + assert.equal(capturedText, "Custom text"); + }); + + it("uses custom buttonText when provided", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({ buttonText: "Open Bridge" }, makeContext()); + assert.equal(capturedOpts.inlineKeyboard[0][0].text, "Open Bridge"); + }); + + it("falls back to sdk.pluginConfig.buttonText when no buttonText param", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "My Bridge Button", startParam: "" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + assert.equal(capturedOpts.inlineKeyboard[0][0].text, "My Bridge Button"); + }); + + it("button URL points to TON Bridge", async () => { + let capturedOpts; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + const url = capturedOpts.inlineKeyboard[0][0].url; + assert.ok(url.includes("TONBridge_robot"), `URL should reference TONBridge_robot, got: ${url}`); + }); + + it("returns failure when sendMessage throws", async () => { + const sdk = makeSdk({ + telegram: { + sendMessage: async () => { throw new Error("Telegram error"); }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + const result = await tool.execute({}, makeContext()); + assert.equal(result.success, false); + assert.ok(result.error); + }); + }); + + describe("ton_bridge_about", () => { + it("returns success when sendMessage succeeds", async () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + const result = await tool.execute({}, makeContext()); + assert.equal(result.success, true); + assert.ok(result.data.message_id != null); + }); + + it("message contains TON Bridge info", async () => { + let capturedText; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text) => { + capturedText = text; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + await tool.execute({}, makeContext()); + assert.ok(capturedText.toLowerCase().includes("bridge"), "about message should mention bridge"); + }); + + it("returns failure when sendMessage throws", async () => { + const sdk = makeSdk({ + telegram: { + sendMessage: async () => { throw new Error("network error"); }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_about"); + const result = await tool.execute({}, makeContext()); + assert.equal(result.success, false); + }); + }); + + describe("ton_bridge_custom_message", () => { + it("sends customMessage as text", async () => { + let capturedText; + const sdk = makeSdk({ + telegram: { + sendMessage: async (chatId, text) => { + capturedText = text; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + await tool.execute({ customMessage: "Hello TON!" }, makeContext()); + assert.equal(capturedText, "Hello TON!"); + }); + + it("returns success with message_id and chat_id", async () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + const result = await tool.execute({ customMessage: "Bridge now" }, makeContext({ chatId: 999 })); + assert.equal(result.success, true); + assert.equal(result.data.chat_id, 999); + assert.equal(result.data.message_id, 42); + }); + + it("returns failure when sendMessage throws", async () => { + const sdk = makeSdk({ + telegram: { + sendMessage: async () => { throw new Error("flood"); }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + const result = await tool.execute({ customMessage: "test" }, makeContext()); + assert.equal(result.success, false); + assert.ok(result.error); + }); + + it("uses customMessage parameter as required", () => { + const sdk = makeSdk(); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_custom_message"); + assert.ok(tool.parameters?.required?.includes("customMessage"), "customMessage should be required"); + }); + }); + + describe("startParam URL building", () => { + it("appends startParam to URL when set", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "Bridge", startParam: "myref" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + const url = capturedOpts.inlineKeyboard[0][0].url; + assert.ok(url.includes("myref"), `URL should include startParam, got: ${url}`); + }); + + it("does not append startParam when empty", async () => { + let capturedOpts; + const sdk = makeSdk({ + pluginConfig: { buttonText: "Bridge", startParam: "" }, + telegram: { + sendMessage: async (chatId, text, opts) => { + capturedOpts = opts; + return 1; + }, + }, + }); + const tool = mod.tools(sdk).find((t) => t.name === "ton_bridge_open"); + await tool.execute({}, makeContext()); + const url = capturedOpts.inlineKeyboard[0][0].url; + // URL should be the base URL without extra params appended via = + assert.ok(url.endsWith("startapp"), `URL without startParam should end with 'startapp', got: ${url}`); + }); + }); +}); diff --git a/plugins/ton-trading-bot/README.md b/plugins/ton-trading-bot/README.md new file mode 100644 index 0000000..d30896d --- /dev/null +++ b/plugins/ton-trading-bot/README.md @@ -0,0 +1,154 @@ +# TON Trading Bot + +Atomic tools for trading on the TON blockchain. The LLM composes these tools into trading strategies — the plugin provides the primitives, not the logic. + +**WARNING: Cryptocurrency trading involves significant financial risk. Do not trade with funds you cannot afford to lose. This plugin does not provide financial advice.** + +## Architecture + +This plugin follows the Teleton tool-provider pattern: + +- **Plugin = atomic tools** (fetch data, validate, simulate, execute) +- **Agent = strategy** (when to buy, when to sell, how much) + +Each tool does exactly one thing. The LLM composes them: + +``` +1. ton_trading_get_market_data → see current prices and DEX quotes +2. ton_trading_get_portfolio → see wallet balance and open positions +3. ton_trading_validate_trade → check risk before acting +4. ton_trading_simulate_trade → paper-trade without real funds +5. ton_trading_execute_swap → execute a real DEX swap (DM-only) +6. ton_trading_record_trade → close a trade and log PnL +``` + +## Tools + +| Tool | Description | Category | +|------|-------------|----------| +| `ton_trading_get_market_data` | Fetch TON price and DEX swap quotes for a pair | data-bearing | +| `ton_trading_get_portfolio` | Wallet balance, jetton holdings, trade history | data-bearing | +| `ton_trading_validate_trade` | Check balance and risk limits before a trade | data-bearing | +| `ton_trading_simulate_trade` | Paper-trade using virtual balance (no real funds) | action | +| `ton_trading_execute_swap` | Execute a real swap on STON.fi or DeDust (DM-only) | action | +| `ton_trading_record_trade` | Close a trade and record final output / PnL | action | + +## Installation + +```bash +mkdir -p ~/.teleton/plugins +cp -r plugins/ton-trading-bot ~/.teleton/plugins/ +``` + +## Configuration + +```yaml +# ~/.teleton/config.yaml +plugins: + ton_trading_bot: + maxTradePercent: 10 # max single trade as % of balance (default: 10) + minBalanceTON: 1 # minimum TON to keep (default: 1) + defaultSlippage: 0.05 # DEX slippage tolerance (default: 5%) + simulationBalance: 1000 # starting virtual balance (default: 1000 TON) +``` + +## Usage Examples + +### Check the market + +``` +Get market data for swapping 1 TON to EQCxE6... +``` + +### Paper-trade workflow + +``` +1. Get market data for TON → USDT +2. Validate trading 5 TON in simulation mode +3. Simulate buying USDT with 5 TON +4. [later] Record the simulated trade closed at price X +``` + +### Real swap workflow (DM only) + +``` +1. Get portfolio overview +2. Get market data for TON → USDT pair +3. Validate trading 2 TON in real mode +4. Execute swap: 2 TON → USDT with 5% slippage +5. [later] Record trade closed +``` + +## Tool Schemas + +### `ton_trading_get_market_data` + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `from_asset` | string | Yes | — | Asset to swap from ("TON" or jetton address) | +| `to_asset` | string | Yes | — | Asset to swap to ("TON" or jetton address) | +| `amount` | string | Yes | — | Amount of from_asset to quote | + +### `ton_trading_get_portfolio` + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `history_limit` | integer | No | 10 | Number of recent trades to include (1–50) | + +### `ton_trading_validate_trade` + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `mode` | string | Yes | — | "real" or "simulation" | +| `amount_ton` | number | Yes | — | Amount of TON being traded | + +### `ton_trading_simulate_trade` + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `from_asset` | string | Yes | — | Asset being sold | +| `to_asset` | string | Yes | — | Asset being bought | +| `amount_in` | number | Yes | — | Amount of from_asset to trade | +| `expected_amount_out` | number | Yes | — | Expected output amount | +| `note` | string | No | — | Optional note for the trade | + +### `ton_trading_execute_swap` + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `from_asset` | string | Yes | — | Asset to sell | +| `to_asset` | string | Yes | — | Asset to buy | +| `amount` | string | Yes | — | Amount to sell | +| `slippage` | number | No | 0.05 | Slippage tolerance (0.001–0.5) | +| `dex` | string | No | auto | "stonfi" or "dedust" | + +### `ton_trading_record_trade` + +| Param | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `trade_id` | integer | Yes | — | Journal trade ID | +| `amount_out` | number | Yes | — | Actual amount received | +| `note` | string | No | — | Optional note (e.g. exit reason) | + +## Risk Management + +Risk parameters are enforced by `ton_trading_validate_trade` before any trade: + +- **maxTradePercent** (default 10%) — no single trade can exceed this percentage of the balance +- **minBalanceTON** (default 1 TON) — trading blocked if balance falls below this floor +- **scope: dm-only** on `ton_trading_execute_swap` — real trades only in direct messages + +The LLM reads the validation result and decides whether to proceed. + +## Database Tables + +- `trade_journal` — every executed and simulated trade with PnL +- `sim_balance` — virtual balance history for paper trading + +## Legal Disclaimer + +**THIS PLUGIN IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. THE DEVELOPERS DO NOT PROVIDE FINANCIAL ADVICE. CRYPTOCURRENCY TRADING IS HIGHLY VOLATILE AND RISKY. YOU ARE RESPONSIBLE FOR YOUR OWN FINANCIAL DECISIONS. USE THIS TOOL AT YOUR OWN RISK.** + +--- + +**Developer:** [xlabtg](https://github.com/xlabtg) diff --git a/plugins/ton-trading-bot/index.js b/plugins/ton-trading-bot/index.js new file mode 100644 index 0000000..0d08673 --- /dev/null +++ b/plugins/ton-trading-bot/index.js @@ -0,0 +1,567 @@ +/** + * TON Trading Bot Plugin + * + * Granular, atomic tools for the LLM to compose trading workflows on TON: + * - ton_trading_get_market_data — fetch current prices and DEX quotes + * - ton_trading_get_portfolio — wallet balance, jetton holdings, trade history + * - ton_trading_validate_trade — check risk parameters before acting + * - ton_trading_simulate_trade — paper-trade without real money + * - ton_trading_execute_swap — execute real swap on TON DEX (DM-only) + * - ton_trading_record_trade — record a closed trade and update PnL + * + * Pattern B (SDK) — uses sdk.ton, sdk.ton.dex, sdk.db, sdk.storage, sdk.log + * + * Architecture: each tool is atomic. The LLM composes them into a strategy. + * No internal signal generation, no embedded strategy loops. + */ + +export const manifest = { + name: "ton-trading-bot", + version: "1.0.0", + sdkVersion: ">=1.0.0", + description: "Atomic TON trading tools: market data, portfolio, risk validation, simulation, and DEX swap execution. The LLM composes these into trading strategies.", + defaultConfig: { + maxTradePercent: 10, // max single trade as % of balance + minBalanceTON: 1, // minimum TON balance required to trade + defaultSlippage: 0.05, // 5% slippage tolerance + simulationBalance: 1000, // starting virtual balance for paper trading + }, +}; + +// ─── Database Migration ────────────────────────────────────────────────────── + +export function migrate(db) { + db.exec(` + -- Trade journal: every executed and simulated trade + CREATE TABLE IF NOT EXISTS trade_journal ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + mode TEXT NOT NULL, -- 'real' | 'simulation' + action TEXT NOT NULL, -- 'buy' | 'sell' + from_asset TEXT NOT NULL, + to_asset TEXT NOT NULL, + amount_in REAL NOT NULL, + amount_out REAL, + pnl REAL, + pnl_percent REAL, + status TEXT NOT NULL, -- 'open' | 'closed' | 'failed' + tx_hash TEXT, + note TEXT + ); + + -- Simulation balance ledger + CREATE TABLE IF NOT EXISTS sim_balance ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + balance REAL NOT NULL + ); + `); +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function getSimBalance(sdk) { + const row = sdk.db + .prepare("SELECT balance FROM sim_balance ORDER BY timestamp DESC LIMIT 1") + .get(); + return row ? row.balance : (sdk.pluginConfig.simulationBalance ?? 1000); +} + +function setSimBalance(sdk, balance) { + sdk.db + .prepare("INSERT INTO sim_balance (timestamp, balance) VALUES (?, ?)") + .run(Date.now(), balance); +} + +// ─── Tools ─────────────────────────────────────────────────────────────────── + +export const tools = (sdk) => [ + + // ── Tool 1: ton_trading_get_market_data ──────────────────────────────────── + { + name: "ton_trading_get_market_data", + description: + "Fetch current TON price and DEX swap quotes for a token pair. Returns raw market data for the LLM to analyze and decide on a trading action.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + from_asset: { + type: "string", + description: 'Asset to swap from — "TON" for native TON, or a jetton master address (e.g. "EQCxE6...")', + }, + to_asset: { + type: "string", + description: 'Asset to swap to — "TON" for native TON, or a jetton master address', + }, + amount: { + type: "string", + description: 'Amount of from_asset to quote (human-readable, e.g. "1" for 1 TON)', + }, + }, + required: ["from_asset", "to_asset", "amount"], + }, + execute: async (params, context) => { + const { from_asset, to_asset, amount } = params; + try { + const [tonPrice, dexQuote] = await Promise.all([ + sdk.ton.getPrice(), + sdk.ton.dex.quote({ + fromAsset: from_asset, + toAsset: to_asset, + amount: parseFloat(amount), + }).catch((err) => { + sdk.log.warn(`DEX quote failed: ${err.message}`); + return null; + }), + ]); + + const walletAddress = sdk.ton.getAddress(); + + const data = { + ton_price_usd: tonPrice?.usd ?? null, + ton_price_source: tonPrice?.source ?? null, + wallet_address: walletAddress, + quote: dexQuote + ? { + from_asset, + to_asset, + amount_in: amount, + stonfi: dexQuote.stonfi ?? null, + dedust: dexQuote.dedust ?? null, + recommended: dexQuote.recommended ?? null, + savings: dexQuote.savings ?? null, + } + : null, + }; + + // Cache for use by validate/simulate tools + sdk.storage.set(`market:${from_asset}:${to_asset}`, data, { ttl: 60_000 }); + + return { success: true, data }; + } catch (err) { + sdk.log.error(`ton_trading_get_market_data failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 2: ton_trading_get_portfolio ────────────────────────────────────── + { + name: "ton_trading_get_portfolio", + description: + "Get the agent's current portfolio: TON balance, jetton (token) holdings, and recent trade history from the journal.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + history_limit: { + type: "integer", + description: "Number of recent trades to include (1–50, default 10)", + minimum: 1, + maximum: 50, + }, + }, + }, + execute: async (params, context) => { + const limit = params.history_limit ?? 10; + try { + const [tonBalance, jettonBalances] = await Promise.all([ + sdk.ton.getBalance(), + sdk.ton.getJettonBalances().catch(() => []), + ]); + + const recentTrades = sdk.db + .prepare( + "SELECT * FROM trade_journal ORDER BY timestamp DESC LIMIT ?" + ) + .all(limit); + + const simBalance = getSimBalance(sdk); + + return { + success: true, + data: { + wallet_address: sdk.ton.getAddress(), + ton_balance: tonBalance?.balance ?? null, + ton_balance_nano: tonBalance?.balanceNano ?? null, + simulation_balance: simBalance, + jetton_holdings: jettonBalances.map((j) => ({ + jetton_address: j.jettonAddress ?? null, + name: j.name ?? null, + symbol: j.symbol ?? null, + balance: j.balanceFormatted ?? j.balance ?? null, + })), + recent_trades: recentTrades, + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_get_portfolio failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 3: ton_trading_validate_trade ───────────────────────────────────── + { + name: "ton_trading_validate_trade", + description: + "Check whether a proposed trade meets risk parameters: balance sufficiency, maximum trade percentage cap, and minimum balance floor. Returns a pass/fail result with reasons. Call this before executing or simulating a trade.", + category: "data-bearing", + parameters: { + type: "object", + properties: { + mode: { + type: "string", + description: 'Trading mode: "real" uses wallet balance, "simulation" uses the virtual balance', + enum: ["real", "simulation"], + }, + amount_ton: { + type: "number", + description: "Amount of TON being traded", + }, + }, + required: ["mode", "amount_ton"], + }, + execute: async (params, context) => { + const { mode, amount_ton } = params; + try { + const balance = + mode === "simulation" + ? getSimBalance(sdk) + : parseFloat((await sdk.ton.getBalance())?.balance ?? "0"); + + const maxTradePercent = sdk.pluginConfig.maxTradePercent ?? 10; + const minBalance = sdk.pluginConfig.minBalanceTON ?? 1; + const maxAllowed = balance * (maxTradePercent / 100); + + const issues = []; + + if (balance < minBalance) { + issues.push({ + type: "insufficient_balance", + message: `Balance (${balance} TON) is below minimum (${minBalance} TON)`, + }); + } + + if (amount_ton > maxAllowed) { + issues.push({ + type: "exceeds_max_trade_percent", + message: `Amount ${amount_ton} TON exceeds ${maxTradePercent}% of balance (max ${maxAllowed.toFixed(4)} TON)`, + }); + } + + if (amount_ton > balance) { + issues.push({ + type: "exceeds_balance", + message: `Amount ${amount_ton} TON exceeds available balance (${balance} TON)`, + }); + } + + const passed = issues.length === 0; + + return { + success: true, + data: { + passed, + mode, + current_balance: balance, + requested_amount: amount_ton, + max_allowed_amount: parseFloat(maxAllowed.toFixed(6)), + issues, + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_validate_trade failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 4: ton_trading_simulate_trade ───────────────────────────────────── + { + name: "ton_trading_simulate_trade", + description: + "Paper-trade (simulate) a swap using the virtual simulation balance. No real funds are spent. Records the simulated trade in the journal.", + category: "action", + parameters: { + type: "object", + properties: { + from_asset: { + type: "string", + description: 'Asset being sold — "TON" or a jetton master address', + }, + to_asset: { + type: "string", + description: 'Asset being bought — "TON" or a jetton master address', + }, + amount_in: { + type: "number", + description: "Amount of from_asset to trade", + }, + expected_amount_out: { + type: "number", + description: "Expected output amount from a prior market data fetch or DEX quote", + }, + note: { + type: "string", + description: "Optional note describing the rationale for this trade", + }, + }, + required: ["from_asset", "to_asset", "amount_in", "expected_amount_out"], + }, + execute: async (params, context) => { + const { from_asset, to_asset, amount_in, expected_amount_out, note } = params; + try { + const simBalance = getSimBalance(sdk); + const minBalance = sdk.pluginConfig.minBalanceTON ?? 1; + + if (from_asset === "TON" && simBalance < amount_in) { + return { + success: false, + error: `Insufficient simulation balance: ${simBalance} TON (need ${amount_in} TON)`, + }; + } + + if (from_asset === "TON" && simBalance - amount_in < minBalance) { + return { + success: false, + error: `Trade would bring simulation balance below minimum (${minBalance} TON)`, + }; + } + + // Update virtual balance: if selling TON, deduct it + if (from_asset === "TON") { + setSimBalance(sdk, simBalance - amount_in); + } + + const tradeId = sdk.db + .prepare( + `INSERT INTO trade_journal + (timestamp, mode, action, from_asset, to_asset, amount_in, amount_out, status, note) + VALUES (?, 'simulation', 'buy', ?, ?, ?, ?, 'open', ?)` + ) + .run(Date.now(), from_asset, to_asset, amount_in, expected_amount_out, note ?? null) + .lastInsertRowid; + + sdk.log.info( + `Simulated trade #${tradeId}: ${amount_in} ${from_asset} → ${expected_amount_out} ${to_asset}` + ); + + return { + success: true, + data: { + trade_id: tradeId, + mode: "simulation", + from_asset, + to_asset, + amount_in, + expected_amount_out, + new_simulation_balance: from_asset === "TON" ? simBalance - amount_in : simBalance, + status: "open", + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_simulate_trade failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 5: ton_trading_execute_swap ─────────────────────────────────────── + { + name: "ton_trading_execute_swap", + description: + "Execute a real token swap on TON DEX (STON.fi / DeDust) from the agent wallet. This spends real funds — call ton_trading_validate_trade first. Only available in direct messages for security.", + category: "action", + scope: "dm-only", + parameters: { + type: "object", + properties: { + from_asset: { + type: "string", + description: 'Asset to sell — "TON" or a jetton master address', + }, + to_asset: { + type: "string", + description: 'Asset to buy — "TON" or a jetton master address', + }, + amount: { + type: "string", + description: 'Amount to sell in human-readable units (e.g. "2.5" for 2.5 TON)', + }, + slippage: { + type: "number", + description: "Slippage tolerance (e.g. 0.05 for 5%). Defaults to plugin config (default: 0.05)", + minimum: 0.001, + maximum: 0.5, + }, + dex: { + type: "string", + description: 'Preferred DEX: "stonfi", "dedust", or omit to use the best available quote', + enum: ["stonfi", "dedust"], + }, + }, + required: ["from_asset", "to_asset", "amount"], + }, + execute: async (params, context) => { + const { + from_asset, + to_asset, + amount, + slippage = sdk.pluginConfig.defaultSlippage ?? 0.05, + dex, + } = params; + + try { + const walletAddress = sdk.ton.getAddress(); + if (!walletAddress) { + return { success: false, error: "Wallet not initialized" }; + } + + const result = await sdk.ton.dex.swap({ + fromAsset: from_asset, + toAsset: to_asset, + amount: parseFloat(amount), + slippage, + ...(dex ? { dex } : {}), + }); + + const tradeId = sdk.db + .prepare( + `INSERT INTO trade_journal + (timestamp, mode, action, from_asset, to_asset, amount_in, amount_out, status) + VALUES (?, 'real', 'buy', ?, ?, ?, ?, 'open')` + ) + .run( + Date.now(), + from_asset, + to_asset, + parseFloat(amount), + result?.expectedOutput ? parseFloat(result.expectedOutput) : null + ) + .lastInsertRowid; + + sdk.log.info( + `Swap executed #${tradeId}: ${amount} ${from_asset} → ${to_asset} via ${result?.dex ?? dex ?? "best"}` + ); + + try { + await sdk.telegram.sendMessage( + context.chatId, + `Swap submitted: ${amount} ${from_asset} → ${to_asset}\nExpected output: ${result?.expectedOutput ?? "unknown"}\nTrade ID: ${tradeId}\nAllow ~30 seconds for on-chain confirmation.` + ); + } catch (msgErr) { + if (msgErr.name === "PluginSDKError") { + sdk.log.warn(`Could not send confirmation message: ${msgErr.code}: ${msgErr.message}`); + } else { + sdk.log.warn(`Could not send confirmation message: ${msgErr.message}`); + } + } + + return { + success: true, + data: { + trade_id: tradeId, + from_asset, + to_asset, + amount_in: amount, + expected_output: result?.expectedOutput ?? null, + min_output: result?.minOutput ?? null, + slippage, + dex: result?.dex ?? dex ?? "auto", + status: "open", + note: "Allow ~30 seconds for on-chain confirmation", + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_execute_swap failed: ${err.message}`); + if (err.name === "PluginSDKError") { + return { success: false, error: `${err.code}: ${String(err.message).slice(0, 500)}` }; + } + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, + + // ── Tool 6: ton_trading_record_trade ─────────────────────────────────────── + { + name: "ton_trading_record_trade", + description: + "Close an open trade in the journal and record the final output amount and PnL. Use this after selling a position to track performance.", + category: "action", + parameters: { + type: "object", + properties: { + trade_id: { + type: "integer", + description: "Journal trade ID returned by ton_trading_execute_swap or ton_trading_simulate_trade", + }, + amount_out: { + type: "number", + description: "Actual amount received when closing the trade", + }, + note: { + type: "string", + description: "Optional note (e.g. exit reason)", + }, + }, + required: ["trade_id", "amount_out"], + }, + execute: async (params, context) => { + const { trade_id, amount_out, note } = params; + try { + const entry = sdk.db + .prepare("SELECT * FROM trade_journal WHERE id = ?") + .get(trade_id); + + if (!entry) { + return { success: false, error: `Trade ${trade_id} not found` }; + } + + if (entry.status === "closed") { + return { success: false, error: `Trade ${trade_id} is already closed` }; + } + + const pnl = amount_out - entry.amount_in; + const pnlPercent = + entry.amount_in > 0 ? (pnl / entry.amount_in) * 100 : 0; + + sdk.db + .prepare( + `UPDATE trade_journal + SET amount_out = ?, pnl = ?, pnl_percent = ?, status = 'closed', note = COALESCE(?, note) + WHERE id = ?` + ) + .run(amount_out, pnl, pnlPercent, note ?? null, trade_id); + + // If simulation, credit the proceeds back + if (entry.mode === "simulation" && entry.to_asset === "TON") { + const simBalance = getSimBalance(sdk); + setSimBalance(sdk, simBalance + amount_out); + } + + sdk.log.info( + `Trade #${trade_id} closed: PnL ${pnl >= 0 ? "+" : ""}${pnl.toFixed(4)} (${pnlPercent.toFixed(2)}%)` + ); + + return { + success: true, + data: { + trade_id, + amount_in: entry.amount_in, + amount_out, + pnl: parseFloat(pnl.toFixed(6)), + pnl_percent: parseFloat(pnlPercent.toFixed(2)), + profit_or_loss: pnl >= 0 ? "profit" : "loss", + mode: entry.mode, + status: "closed", + }, + }; + } catch (err) { + sdk.log.error(`ton_trading_record_trade failed: ${err.message}`); + return { success: false, error: String(err.message).slice(0, 500) }; + } + }, + }, +]; diff --git a/plugins/ton-trading-bot/manifest.json b/plugins/ton-trading-bot/manifest.json new file mode 100644 index 0000000..3b45a0e --- /dev/null +++ b/plugins/ton-trading-bot/manifest.json @@ -0,0 +1,50 @@ +{ + "id": "ton-trading-bot", + "name": "TON Trading Bot", + "version": "1.0.0", + "description": "Atomic TON trading tools: market data, portfolio, risk validation, simulation, and DEX swap execution. The LLM composes these into trading strategies.", + "author": { + "name": "xlabtg", + "url": "https://github.com/xlabtg" + }, + "license": "MIT", + "entry": "index.js", + "teleton": ">=1.0.0", + "sdkVersion": ">=1.0.0", + "tools": [ + { + "name": "ton_trading_get_market_data", + "description": "Fetch current TON price and DEX swap quotes for a token pair" + }, + { + "name": "ton_trading_get_portfolio", + "description": "Get wallet balance, jetton holdings, and recent trade history" + }, + { + "name": "ton_trading_validate_trade", + "description": "Check risk parameters before a trade (balance, max trade %, minimum balance)" + }, + { + "name": "ton_trading_simulate_trade", + "description": "Paper-trade a swap using the virtual simulation balance (no real funds)" + }, + { + "name": "ton_trading_execute_swap", + "description": "Execute a real DEX swap (STON.fi / DeDust) from the agent wallet — DM only" + }, + { + "name": "ton_trading_record_trade", + "description": "Close an open trade in the journal and record final PnL" + } + ], + "defaultConfig": { + "maxTradePercent": 10, + "minBalanceTON": 1, + "defaultSlippage": 0.05, + "simulationBalance": 1000 + }, + "permissions": [], + "tags": ["trading", "ton", "dex", "portfolio", "simulation"], + "repository": "https://github.com/xlabtg/teleton-plugins", + "funding": null +} diff --git a/plugins/twitter/manifest.json b/plugins/twitter/manifest.json index 43dd3a8..2a825f7 100644 --- a/plugins/twitter/manifest.json +++ b/plugins/twitter/manifest.json @@ -41,6 +41,7 @@ { "name": "twitter_bookmark", "description": "Bookmark a tweet (OAuth)" }, { "name": "twitter_remove_bookmark", "description": "Remove a bookmark (OAuth)" } ], + "permissions": [], "tags": ["social", "twitter", "x", "search", "trends", "oauth"], "repository": "https://github.com/TONresistor/teleton-plugins", "funding": null diff --git a/registry.json b/registry.json index c8b8ac6..a1a26f9 100644 --- a/registry.json +++ b/registry.json @@ -184,6 +184,30 @@ "author": "teleton", "tags": ["forum", "discussion", "ton", "decentralized", "x402", "boards"], "path": "plugins/boards" + }, + { + "id": "ton-trading-bot", + "name": "TON Trading Bot", + "description": "Atomic TON trading tools: market data, portfolio, risk validation, simulation, and DEX swap execution", + "author": "xlabtg", + "tags": ["trading", "ton", "dex", "portfolio", "simulation"], + "path": "plugins/ton-trading-bot" + }, + { + "id": "ton-bridge", + "name": "TON Bridge", + "description": "Beautiful inline button plugin for TON Bridge Mini App access", + "author": "xlabtg", + "tags": ["ton", "bridge", "miniapp", "tool", "tonbridge"], + "path": "plugins/ton-bridge" + }, + { + "id": "github-dev-assistant", + "name": "GitHub Dev Assistant", + "description": "Full GitHub development workflow automation — repos, files, branches, PRs, issues, and GitHub Actions via Personal Access Token", + "author": "xlabtg", + "tags": ["github", "development", "automation", "git", "ci-cd"], + "path": "plugins/github-dev-assistant" } ] } diff --git a/scripts/build-sdk.mjs b/scripts/build-sdk.mjs new file mode 100644 index 0000000..c367fe8 --- /dev/null +++ b/scripts/build-sdk.mjs @@ -0,0 +1,192 @@ +/** + * build-sdk.mjs + * + * Validates SDK plugins (those that export tools as a function) and + * generates TypeScript declaration files (.d.ts) for the plugin interface. + * + * Used by CI / Build (SDK with DTS) workflow. + */ + +import { readdir, readFile, mkdir, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const PLUGINS_DIR = resolve("plugins"); +const DIST_DIR = resolve("dist"); + +// Minimal mock SDK for initialization +const MOCK_SDK = { + ton: { + getAddress: () => null, + getPublicKey: () => null, + getWalletVersion: () => "v5r1", + getBalance: async () => null, + getPrice: async () => null, + sendTON: async () => { throw new Error("mock"); }, + getTransactions: async () => [], + verifyPayment: async () => ({ verified: false }), + getJettonBalances: async () => [], + getJettonInfo: async () => null, + sendJetton: async () => { throw new Error("mock"); }, + createJettonTransfer: async () => { throw new Error("mock"); }, + getJettonWalletAddress: async () => null, + getNftItems: async () => [], + getNftInfo: async () => null, + toNano: (v) => BigInt(Math.round(parseFloat(v) * 1e9)), + fromNano: (v) => String(Number(v) / 1e9), + validateAddress: () => false, + getJettonPrice: async () => null, + getJettonHolders: async () => [], + getJettonHistory: async () => null, + dex: { + quote: async () => { throw new Error("mock"); }, + quoteSTONfi: async () => null, + quoteDeDust: async () => null, + swap: async () => { throw new Error("mock"); }, + swapSTONfi: async () => { throw new Error("mock"); }, + swapDeDust: async () => { throw new Error("mock"); }, + }, + dns: { + check: async () => ({ available: false }), + resolve: async () => null, + getAuctions: async () => [], + startAuction: async () => { throw new Error("mock"); }, + bid: async () => { throw new Error("mock"); }, + link: async () => { throw new Error("mock"); }, + unlink: async () => { throw new Error("mock"); }, + setSiteRecord: async () => { throw new Error("mock"); }, + }, + }, + telegram: { + sendMessage: async () => 0, + editMessage: async () => 0, + deleteMessage: async () => {}, + getMessages: async () => [], + getMe: async () => null, + isAvailable: () => false, + getRawClient: () => null, + }, + bot: { + onInlineQuery: () => {}, + onCallback: () => {}, + answerInline: async () => {}, + answerCallback: async () => {}, + }, + db: null, + log: { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }, + pluginConfig: {}, + secrets: { get: async () => null }, +}; + +/** + * Generate a TypeScript declaration file for a plugin's tools. + */ +function generateDts(pluginName, toolList) { + const toolInterfaces = toolList.map((tool) => { + const name = tool.name ?? "unknown"; + const description = (tool.description ?? "").replace(/\*\//g, "* /"); + return ` /** ${description} */\n readonly ${JSON.stringify(name)}: ToolDefinition;`; + }).join("\n"); + + return `// Auto-generated by build-sdk.mjs — do not edit manually +// Plugin: ${pluginName} + +export interface ToolDefinition { + name: string; + description: string; + parameters?: Record; + execute: (params: Record, context: unknown) => Promise; + scope?: "always" | "dm-only" | "group-only" | "admin-only"; + category?: "data-bearing" | "action"; +} + +export interface ToolResult { + success: boolean; + data?: Record; + error?: string; +} + +export interface Plugin { + tools: ToolDefinition[] | ((sdk: unknown) => ToolDefinition[]); +} + +export declare const tools: ToolDefinition[] | ((sdk: unknown) => ToolDefinition[]); + +export declare const toolMap: { +${toolInterfaces} +}; +`; +} + +let errors = 0; +let sdkPlugins = 0; + +const entries = await readdir(PLUGINS_DIR, { withFileTypes: true }); +const pluginDirs = entries + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + +await mkdir(DIST_DIR, { recursive: true }); + +console.log(`\nBuilding SDK plugins from ${pluginDirs.length} total plugins...\n`); + +for (const name of pluginDirs) { + const dir = join(PLUGINS_DIR, name); + const indexPath = join(dir, "index.js"); + + if (!existsSync(indexPath)) continue; + + let mod; + try { + mod = await import(pathToFileURL(indexPath).href); + } catch (e) { + // If the import fails, skip this plugin (already reported in build-runtime) + console.log(` [SKIP] ${name}: import failed (missing deps?)`); + continue; + } + + if (!mod.tools) continue; + + // Only process SDK plugins (tools as function) + if (typeof mod.tools !== "function") { + console.log(` [SKIP] ${name}: not an SDK plugin`); + continue; + } + + sdkPlugins++; + + let toolList; + try { + toolList = mod.tools(MOCK_SDK); + } catch (e) { + console.error(` [ERROR] ${name}: tools(sdk) threw: ${e.message}`); + errors++; + continue; + } + + if (!Array.isArray(toolList)) { + console.error(` [ERROR] ${name}: tools(sdk) did not return an array`); + errors++; + continue; + } + + // Generate .d.ts file + const dtsContent = generateDts(name, toolList); + const dtsPath = join(DIST_DIR, `${name}.d.ts`); + await writeFile(dtsPath, dtsContent, "utf8"); + + console.log(` [OK] ${name}: ${toolList.length} tool(s) → dist/${name}.d.ts`); +} + +console.log(`\nResult: ${sdkPlugins} SDK plugin(s) processed, ${errors} error(s)\n`); + +if (errors > 0) { + process.exit(1); +} diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs new file mode 100644 index 0000000..9948551 --- /dev/null +++ b/scripts/run-tests.mjs @@ -0,0 +1,83 @@ +/** + * run-tests.mjs + * + * Discovers and runs test files in plugin directories using Node's built-in + * test runner (node:test). Tests must be in: + * plugins//tests/*.test.js + * plugins//tests/*.test.mjs + * plugins//*.test.js + * plugins//*.test.mjs + * + * Also runs any scripts/**.test.mjs files. + * + * Used by CI / Test workflow. + */ + +import { readdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { spawnSync } from "node:child_process"; + +const PLUGINS_DIR = resolve("plugins"); +const SCRIPTS_DIR = resolve("scripts"); + +const testFiles = []; + +// Discover plugin test files +const entries = await readdir(PLUGINS_DIR, { withFileTypes: true }); +for (const entry of entries) { + if (!entry.isDirectory()) continue; + const dir = join(PLUGINS_DIR, entry.name); + + // Check tests/ subdirectory + const testsDir = join(dir, "tests"); + if (existsSync(testsDir)) { + const testEntries = await readdir(testsDir); + for (const f of testEntries) { + if (f.endsWith(".test.js") || f.endsWith(".test.mjs")) { + testFiles.push(join(testsDir, f)); + } + } + } + + // Check plugin root + const rootEntries = await readdir(dir); + for (const f of rootEntries) { + if (f.endsWith(".test.js") || f.endsWith(".test.mjs")) { + testFiles.push(join(dir, f)); + } + } +} + +// Discover scripts test files +if (existsSync(SCRIPTS_DIR)) { + const scriptEntries = await readdir(SCRIPTS_DIR); + for (const f of scriptEntries) { + if (f.endsWith(".test.js") || f.endsWith(".test.mjs")) { + testFiles.push(join(SCRIPTS_DIR, f)); + } + } +} + +if (testFiles.length === 0) { + console.log("No test files found. Skipping."); + process.exit(0); +} + +console.log(`\nFound ${testFiles.length} test file(s):\n`); +for (const f of testFiles) { + console.log(` ${f}`); +} +console.log(); + +// Run all tests with Node's built-in test runner +const result = spawnSync( + process.execPath, + ["--test", ...testFiles], + { + stdio: "inherit", + env: { ...process.env, NODE_ENV: "test" }, + } +); + +process.exit(result.status ?? 1); diff --git a/scripts/validate-plugins.mjs b/scripts/validate-plugins.mjs new file mode 100644 index 0000000..3137438 --- /dev/null +++ b/scripts/validate-plugins.mjs @@ -0,0 +1,264 @@ +/** + * validate-plugins.mjs + * + * Validates that every plugin in the plugins/ directory: + * 1. Has a manifest.json with required fields + * 2. Has an index.js that exports `tools` (array or function) + * 3. Tools have required fields: name, description, execute + * + * Used by CI / Build (Runtime) workflow. + */ + +import { readdir, readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { resolve, join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const PLUGINS_DIR = resolve("plugins"); +const REQUIRED_MANIFEST_FIELDS = [ + "id", + "name", + "version", + "description", + "author", + "license", + "entry", + "teleton", + "tools", + "permissions", +]; + +// Minimal mock SDK for plugins that export tools(sdk) +const MOCK_SDK = { + ton: { + getAddress: () => null, + getPublicKey: () => null, + getWalletVersion: () => "v5r1", + getBalance: async () => null, + getPrice: async () => null, + sendTON: async () => { throw new Error("mock"); }, + getTransactions: async () => [], + verifyPayment: async () => ({ verified: false }), + getJettonBalances: async () => [], + getJettonInfo: async () => null, + sendJetton: async () => { throw new Error("mock"); }, + createJettonTransfer: async () => { throw new Error("mock"); }, + getJettonWalletAddress: async () => null, + getNftItems: async () => [], + getNftInfo: async () => null, + toNano: (v) => BigInt(Math.round(parseFloat(v) * 1e9)), + fromNano: (v) => String(Number(v) / 1e9), + validateAddress: () => false, + getJettonPrice: async () => null, + getJettonHolders: async () => [], + getJettonHistory: async () => null, + dex: { + quote: async () => { throw new Error("mock"); }, + quoteSTONfi: async () => null, + quoteDeDust: async () => null, + swap: async () => { throw new Error("mock"); }, + swapSTONfi: async () => { throw new Error("mock"); }, + swapDeDust: async () => { throw new Error("mock"); }, + }, + dns: { + check: async () => ({ available: false }), + resolve: async () => null, + getAuctions: async () => [], + startAuction: async () => { throw new Error("mock"); }, + bid: async () => { throw new Error("mock"); }, + link: async () => { throw new Error("mock"); }, + unlink: async () => { throw new Error("mock"); }, + setSiteRecord: async () => { throw new Error("mock"); }, + }, + }, + telegram: { + sendMessage: async () => 0, + editMessage: async () => 0, + deleteMessage: async () => {}, + forwardMessage: async () => 0, + pinMessage: async () => {}, + sendDice: async () => ({ value: 1, messageId: 0 }), + sendReaction: async () => {}, + getMessages: async () => [], + searchMessages: async () => [], + getReplies: async () => [], + scheduleMessage: async () => 0, + getScheduledMessages: async () => [], + deleteScheduledMessage: async () => {}, + sendScheduledNow: async () => {}, + getDialogs: async () => [], + getHistory: async () => [], + getMe: async () => null, + isAvailable: () => false, + getRawClient: () => null, + sendPhoto: async () => 0, + sendVideo: async () => 0, + sendVoice: async () => 0, + sendFile: async () => 0, + sendGif: async () => 0, + sendSticker: async () => 0, + downloadMedia: async () => null, + setTyping: async () => {}, + getChatInfo: async () => null, + getUserInfo: async () => null, + resolveUsername: async () => null, + getParticipants: async () => [], + createPoll: async () => 0, + createQuiz: async () => 0, + banUser: async () => {}, + unbanUser: async () => {}, + muteUser: async () => {}, + kickUser: async () => {}, + getStarsBalance: async () => 0, + sendGift: async () => {}, + getAvailableGifts: async () => [], + getMyGifts: async () => [], + getResaleGifts: async () => [], + buyResaleGift: async () => {}, + getStarsTransactions: async () => [], + transferCollectible: async () => { throw new Error("mock"); }, + setCollectiblePrice: async () => {}, + getCollectibleInfo: async () => null, + getUniqueGift: async () => null, + getUniqueGiftValue: async () => null, + sendGiftOffer: async () => {}, + sendStory: async () => 0, + }, + bot: { + onInlineQuery: () => {}, + onCallback: () => {}, + answerInline: async () => {}, + answerCallback: async () => {}, + }, + db: null, + log: { + info: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {}, + }, + pluginConfig: {}, + secrets: { get: async () => null }, +}; + +let errors = 0; +let warnings = 0; + +function error(plugin, msg) { + console.error(` [ERROR] ${plugin}: ${msg}`); + errors++; +} + +function warn(plugin, msg) { + console.warn(` [WARN] ${plugin}: ${msg}`); + warnings++; +} + +function ok(plugin, msg) { + console.log(` [OK] ${plugin}: ${msg}`); +} + +const entries = await readdir(PLUGINS_DIR, { withFileTypes: true }); +const pluginDirs = entries + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + +console.log(`\nValidating ${pluginDirs.length} plugins...\n`); + +for (const name of pluginDirs) { + const dir = join(PLUGINS_DIR, name); + const manifestPath = join(dir, "manifest.json"); + const indexPath = join(dir, "index.js"); + + process.stdout.write(`Plugin: ${name}\n`); + + // 1. Check manifest.json + if (!existsSync(manifestPath)) { + error(name, "missing manifest.json"); + continue; + } + + let manifest; + try { + manifest = JSON.parse(await readFile(manifestPath, "utf8")); + } catch (e) { + error(name, `invalid manifest.json JSON: ${e.message}`); + continue; + } + + for (const field of REQUIRED_MANIFEST_FIELDS) { + if (manifest[field] === undefined) { + error(name, `manifest.json missing required field: ${field}`); + } + } + + if (manifest.id && manifest.id !== name) { + error(name, `manifest.json id "${manifest.id}" does not match folder name "${name}"`); + } + + // 2. Check index.js exists + if (!existsSync(indexPath)) { + error(name, "missing index.js"); + continue; + } + + // 3. Import and validate exports + let mod; + try { + mod = await import(pathToFileURL(indexPath).href); + } catch (e) { + error(name, `failed to import index.js: ${e.message}`); + continue; + } + + if (!mod.tools) { + error(name, "index.js does not export `tools`"); + continue; + } + + // 4. Resolve tools (array or function) + let toolList; + if (typeof mod.tools === "function") { + try { + toolList = mod.tools(MOCK_SDK); + } catch (e) { + error(name, `tools(sdk) threw during initialization: ${e.message}`); + continue; + } + if (!Array.isArray(toolList)) { + error(name, "tools(sdk) must return an array"); + continue; + } + } else if (Array.isArray(mod.tools)) { + toolList = mod.tools; + } else { + error(name, "`tools` export must be an array or a function returning an array"); + continue; + } + + if (toolList.length === 0) { + warn(name, "tools array is empty"); + } + + // 5. Validate each tool + for (const tool of toolList) { + if (!tool.name) { + error(name, `tool missing required field: name`); + } + if (!tool.description) { + error(name, `tool "${tool.name ?? "?"}" missing required field: description`); + } + if (typeof tool.execute !== "function") { + error(name, `tool "${tool.name ?? "?"}" missing required field: execute (must be a function)`); + } + } + + ok(name, `${toolList.length} tool(s) validated`); +} + +console.log(`\nResult: ${pluginDirs.length} plugins, ${errors} error(s), ${warnings} warning(s)\n`); + +if (errors > 0) { + process.exit(1); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..76c3a21 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "allowJs": true, + "checkJs": false, + "strict": false, + "noEmit": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": [ + "scripts/**/*.mjs", + "scripts/**/*.ts" + ], + "exclude": [ + "node_modules", + "plugins/*/node_modules", + "dist" + ] +}