From 4e1870d7b5e69428bd1ab6fab38648f67385c395 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Tue, 4 Mar 2025 19:07:06 +0800 Subject: [PATCH] feat: x services (#43) * feat: twitter services * fix: package.json * fix: type error * refactor: launcher * refactor: BrowserBase/Stagehand * fix: config & startup * chore: i18n * fix: continue i18n * feat: cookie login * fix: save cookie to session json * docs: architecture * chore: remove cli & use mcp adapter for mcp-server * refactor: remove browser adapter, use playwright directly * refactor: remove launcher services, replace twitter.com to x.com * feat: load session to auto login * docs: update architecture md * chore: more debug logs * feat: tweet parser without hast and rehype * fix: type error * fix: env example --- .gitignore | 2 + cspell.config.yaml | 1 + pnpm-lock.yaml | 766 ++++++++++++++++-- services/twitter-services/.env.example | 21 + .../twitter-services/docs/architecture.md | 396 +++++++++ services/twitter-services/package.json | 31 + .../src/adapters/airi-adapter.ts | 5 + .../src/adapters/mcp-adapter.ts | 577 +++++++++++++ services/twitter-services/src/config/index.ts | 113 +++ services/twitter-services/src/config/types.ts | 91 +++ .../twitter-services/src/core/auth-service.ts | 614 ++++++++++++++ .../src/core/timeline-service.ts | 118 +++ .../src/core/twitter-service.ts | 222 +++++ services/twitter-services/src/main.ts | 188 +++++ .../src/parsers/profile-parser.ts | 293 +++++++ .../src/parsers/tweet-parser.ts | 260 ++++++ .../twitter-services/src/types/twitter.ts | 85 ++ services/twitter-services/src/utils/error.ts | 74 ++ services/twitter-services/src/utils/logger.ts | 72 ++ .../src/utils/rate-limiter.ts | 67 ++ .../twitter-services/src/utils/selectors.ts | 83 ++ services/twitter-services/tsconfig.json | 25 + 22 files changed, 4030 insertions(+), 74 deletions(-) create mode 100644 services/twitter-services/.env.example create mode 100644 services/twitter-services/docs/architecture.md create mode 100644 services/twitter-services/package.json create mode 100644 services/twitter-services/src/adapters/airi-adapter.ts create mode 100644 services/twitter-services/src/adapters/mcp-adapter.ts create mode 100644 services/twitter-services/src/config/index.ts create mode 100644 services/twitter-services/src/config/types.ts create mode 100644 services/twitter-services/src/core/auth-service.ts create mode 100644 services/twitter-services/src/core/timeline-service.ts create mode 100644 services/twitter-services/src/core/twitter-service.ts create mode 100644 services/twitter-services/src/main.ts create mode 100644 services/twitter-services/src/parsers/profile-parser.ts create mode 100644 services/twitter-services/src/parsers/tweet-parser.ts create mode 100644 services/twitter-services/src/types/twitter.ts create mode 100644 services/twitter-services/src/utils/error.ts create mode 100644 services/twitter-services/src/utils/logger.ts create mode 100644 services/twitter-services/src/utils/rate-limiter.ts create mode 100644 services/twitter-services/src/utils/selectors.ts create mode 100644 services/twitter-services/tsconfig.json diff --git a/.gitignore b/.gitignore index 8ac53d4e..5e715f34 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ coverage/ *.mp3 **/temp/ + +twitter-session.json diff --git a/cspell.config.yaml b/cspell.config.yaml index f5331861..170864c3 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -15,6 +15,7 @@ words: - baiducloud - bigserial - Bitstream + - browserbasehq - bumpp - catppuccin - changelogithub diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cd364e2..260d10f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -378,7 +378,7 @@ importers: version: 2.3.0 '@intlify/unplugin-vue-i18n': specifier: ^6.0.3 - version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.8.2)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) + version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) '@proj-airi/elevenlabs': specifier: workspace:^ version: link:../../packages/elevenlabs @@ -405,10 +405,10 @@ importers: version: 5.2.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)) '@vue-macros/volar': specifier: ^0.30.15 - version: 0.30.15(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) + version: 0.30.15(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) '@vueuse/motion': specifier: ^2.2.6 - version: 2.2.6(magicast@0.3.5)(rollup@4.34.9)(vue@3.5.13(typescript@5.8.2)) + version: 2.2.6(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2)) electron: specifier: ^34.3.0 version: 34.3.0 @@ -426,28 +426,28 @@ importers: version: 3.2.0(unocss@66.1.0-beta.3(postcss@8.5.3)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))) unplugin-auto-import: specifier: ^19.1.1 - version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(@vueuse/core@12.7.0(typescript@5.8.2)) + version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.7.0(typescript@5.8.2)) unplugin-vue-components: specifier: ^28.4.1 - version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.8.2)) + version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2)) unplugin-vue-macros: specifier: ^2.14.5 - version: 2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.25.0)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) + version: 2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) unplugin-vue-markdown: specifier: ^28.3.1 version: 28.3.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) unplugin-vue-router: specifier: ^0.11.2 - version: 0.11.2(rollup@4.34.9)(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) + version: 0.11.2(rollup@2.79.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) vite-bundle-visualizer: specifier: ^1.2.1 - version: 1.2.1(rollup@4.34.9) + version: 1.2.1(rollup@2.79.1) vite-plugin-pwa: specifier: ^0.21.1 - version: 0.21.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0)(workbox-window@7.3.0) + version: 0.21.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-vue-devtools: specifier: ^7.7.2 - version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(rollup@4.34.9)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)) + version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)) vite-plugin-vue-layouts: specifier: ^0.11.0 version: 0.11.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) @@ -685,7 +685,7 @@ importers: version: 2.3.0 '@intlify/unplugin-vue-i18n': specifier: ^6.0.3 - version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.2)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) + version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.21.0(jiti@2.4.2))(rollup@4.34.9)(typescript@5.8.2)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) '@proj-airi/drizzle-duckdb-wasm': specifier: workspace:^ version: link:../../packages/drizzle-duckdb-wasm @@ -721,10 +721,10 @@ importers: version: 5.2.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)) '@vue-macros/volar': specifier: ^0.30.15 - version: 0.30.15(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) + version: 0.30.15(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) '@vueuse/motion': specifier: ^2.2.6 - version: 2.2.6(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.2)) + version: 2.2.6(magicast@0.3.5)(rollup@4.34.9)(vue@3.5.13(typescript@5.8.2)) hfup: specifier: workspace:^ version: link:../../packages/hfup @@ -733,28 +733,28 @@ importers: version: 4.0.1 unplugin-auto-import: specifier: ^19.1.1 - version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@12.7.0(typescript@5.8.2)) + version: 19.1.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(@vueuse/core@12.7.0(typescript@5.8.2)) unplugin-vue-components: specifier: ^28.4.1 - version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.2)) + version: 28.4.1(@babel/parser@7.26.7)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.8.2)) unplugin-vue-macros: specifier: ^2.14.5 - version: 2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) + version: 2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.19.12)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) unplugin-vue-markdown: specifier: ^28.3.1 version: 28.3.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) unplugin-vue-router: specifier: ^0.11.2 - version: 0.11.2(rollup@2.79.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) + version: 0.11.2(rollup@4.34.9)(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) vite-bundle-visualizer: specifier: ^1.2.1 - version: 1.2.1(rollup@2.79.1) + version: 1.2.1(rollup@4.34.9) vite-plugin-pwa: specifier: ^0.21.1 - version: 0.21.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0)(workbox-window@7.3.0) + version: 0.21.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-vue-devtools: specifier: ^7.7.2 - version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)) + version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(rollup@4.34.9)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)) vite-plugin-vue-layouts: specifier: ^0.11.0 version: 0.11.0(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) @@ -1297,6 +1297,52 @@ importers: specifier: ^4.19.3 version: 4.19.3 + services/twitter-services: + dependencies: + '@browserbasehq/stagehand': + specifier: ^1.13.1 + version: 1.13.1(@playwright/test@1.50.1)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@16.4.7)(encoding@0.1.13)(openai@4.85.0(encoding@0.1.13)(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.24.2))(utf-8-validate@5.0.10)(zod@3.24.2) + '@guiiai/logg': + specifier: ^1.0.0 + version: 1.0.7 + '@modelcontextprotocol/sdk': + specifier: ^1.6.1 + version: 1.6.1 + '@proj-airi/server-sdk': + specifier: ^0.1.0 + version: 0.1.4 + defu: + specifier: ^6.1.4 + version: 6.1.4 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + h3: + specifier: ^1.11.0 + version: 1.15.1 + listhen: + specifier: ^1.6.0 + version: 1.9.0 + ofetch: + specifier: ^1.3.3 + version: 1.4.1 + playwright: + specifier: ^1.50.1 + version: 1.50.1 + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@types/node': + specifier: ^18.16.3 + version: 18.19.76 + tsx: + specifier: ^4.19.0 + version: 4.19.3 + typescript: + specifier: ^5.0.4 + version: 5.8.2 + packages: 7zip-bin@5.2.0: @@ -1375,6 +1421,9 @@ packages: '@antfu/utils@8.1.0': resolution: {integrity: sha512-XPR7Jfwp0FFl/dFYPX8ZjpmU4/1mIXTjnZ1ba48BLMyKOV62/tiRjdsFcPs2hsYcSud4tzk7w3a3LjX8Fu3huA==} + '@anthropic-ai/sdk@0.27.3': + resolution: {integrity: sha512-IjLt0gd3L4jlOfilxVXTifn42FnVffMgDC04RJK1KDZpmkBWLv0XC92MVVmkxrFZNS/7l3xWgP/I3nqtX1sQHw==} + '@apideck/better-ajv-errors@0.3.6': resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} @@ -1963,6 +2012,18 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@browserbasehq/sdk@2.3.0': + resolution: {integrity: sha512-H2nu46C6ydWgHY+7yqaP8qpfRJMJFVGxVIgsuHe1cx9HkfJHqzkuIqaK/k8mU4ZeavQgV5ZrJa0UX6MDGYiT4w==} + + '@browserbasehq/stagehand@1.13.1': + resolution: {integrity: sha512-sty9bDiuuQJDOS+/uBfXpwYQY+mhFyqi6uT5wSOrazagZ5s8tgk3ryCIheB/BGS5iisc6ivAsKe9aC9n5WBTAg==} + peerDependencies: + '@playwright/test': ^1.42.1 + deepmerge: ^4.3.1 + dotenv: ^16.4.5 + openai: ^4.62.1 + zod: ^3.23.8 + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -3258,6 +3319,10 @@ packages: '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + '@modelcontextprotocol/sdk@1.6.1': + resolution: {integrity: sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==} + engines: {node: '>=18'} + '@mswjs/interceptors@0.37.6': resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} engines: {node: '>=18'} @@ -3861,12 +3926,23 @@ packages: resolution: {integrity: sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.50.1': + resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.24': resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} + '@proj-airi/server-sdk@0.1.4': + resolution: {integrity: sha512-iyOtnFcQJL0xs+BZ8flDbxkCdrS5kMZrLBmQI//fmYj3yV0ny7TpVwvvPsJaqtcDfrYDOjmQ+JOu4R/YntHVxg==} + '@proj-airi/server-sdk@0.3.6': resolution: {integrity: sha512-jpUxVoqrEA0qt7Z24PpjHg9R7J6PzB/GAgRgjaHOzEF3MHwg6FqQludnmbqUo+gkhn1GIynVMzoVTOujsPI2tg==} + '@proj-airi/server-shared@0.1.4': + resolution: {integrity: sha512-cVqVqLqvC9n8HcXQMm6zKA8W0eTGpcyhdn5finIZr9fyHo6pdhAZN62ER+Nq8FP4uWwbf4uiDnWdVHN+ZwZJHQ==} + '@proj-airi/server-shared@0.3.6': resolution: {integrity: sha512-VVGhVHDrZojH2RQIC6w4znmA/QlWrxFL3UI1N/pQU20vpRN23r4NaG/XvxdlzxQOYuXkyHvdc6/gn1b+qJnBpA==} @@ -4233,6 +4309,18 @@ packages: '@types/audioworklet@0.0.71': resolution: {integrity: sha512-bi5UKn9UXQaDPN9C3gVIT7D1CbPRlJyUPqfTqpLOUVh2cRoPdCscrVPuBkXiJV2JvHH/4bXvmYDSg7SmC7wuXA==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} @@ -5172,9 +5260,15 @@ packages: '@xsai/providers@0.1.0-beta.5': resolution: {integrity: sha512-jx630mUblMSUcDcXa+BNiFB7deQy1lp0XcJHQokfo/+QvYOpgaPrIRH6oUcjwLio0+8gJXqQmQfEY3+fl9Tjew==} + '@xsai/shared-chat@0.1.0': + resolution: {integrity: sha512-LGF11CBDEWGs5NsCtQ85k9q/jHPzEEFFtScZR9gd7Nxpui5g/TTrgcDMW9Tz0oLHQBPGK31sqQcLYbC7KXjkOw==} + '@xsai/shared-chat@0.1.0-beta.9': resolution: {integrity: sha512-ctb5LV0oig/8ZsjVVSSYcR64CMXtsSYZ3gnVwLGGkBhKpD3UB5a55FFPaHoZA9Iqucnkw/AhWz3xwgWhOJbvqA==} + '@xsai/shared@0.1.0': + resolution: {integrity: sha512-Amr/+IAFqdWei2ec0uy4a8vpSl3QA3J+dvC7nX+oENokU2+204l7WIkT8l2iUabw0mJjjQ8SlMMh/7YSe0vVxg==} + '@xsai/shared@0.1.0-beta.9': resolution: {integrity: sha512-1QIL042SdDcuZt+tJQmK9VbW62KOHKKC3N9aYNE3ZXu+3dRiYgMUewpW0os7u/H1RPb9BzLLB/5r0cN5G8ijNA==} @@ -5198,6 +5292,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -5519,6 +5617,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.1.0: + resolution: {integrity: sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5628,10 +5730,18 @@ packages: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -5911,6 +6021,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -5927,6 +6041,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -6202,6 +6320,15 @@ packages: supports-color: optional: true + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -6493,6 +6620,10 @@ packages: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -6512,6 +6643,11 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + ejs@3.1.9: resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} engines: {node: '>=0.10.0'} @@ -6640,6 +6776,10 @@ packages: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -6647,6 +6787,10 @@ packages: es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} @@ -6997,6 +7141,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.0: + resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.5: + resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -7013,10 +7165,20 @@ packages: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.0: + resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} + engines: {node: '>= 16'} + peerDependencies: + express: ^4.11 || 5 || ^5.0.0-beta.1 + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.0.1: + resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} + engines: {node: '>= 18'} + expressive-code@0.40.1: resolution: {integrity: sha512-jBsTRX+MPsqiqYQsE9vRXMiAkUafU11j2zuWAaOX9vubLutNB0er8c0FJWeudVDH5D52V4Lf4vTIqbOE54PUcQ==} @@ -7113,6 +7275,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.0.0: + resolution: {integrity: sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==} + engines: {node: '>= 0.8'} + find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -7214,6 +7380,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -7286,12 +7456,20 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -7394,6 +7572,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} @@ -7444,6 +7626,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} @@ -7462,6 +7648,10 @@ packages: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -7630,6 +7820,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.5.2: + resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -7824,6 +8018,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -8308,6 +8505,10 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-definitions@6.0.0: resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} @@ -8378,9 +8579,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -8511,10 +8720,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.53.0: + resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.0: + resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -8681,6 +8898,9 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8735,6 +8955,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -9108,6 +9332,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@5.0.0: resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} engines: {node: '>=12'} @@ -9224,6 +9452,10 @@ packages: '@pixi/sprite': ^6 '@pixi/utils': ^6 + pkce-challenge@4.1.0: + resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -9663,6 +9895,10 @@ packages: resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} engines: {node: '>=0.6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.6: resolution: {integrity: sha512-u3TuxVTuJtkTxKGk5oZ7K2/o+l0/cC6J8SOyaaSnrnroqvcVy7xBxtvBUyd+Xa8cGoCr87XmQj4NR6W+zbqH8w==} @@ -9702,6 +9938,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -9977,6 +10217,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.1.0: + resolution: {integrity: sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==} + engines: {node: '>= 18'} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -10050,6 +10294,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.1.0: + resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} + engines: {node: '>= 18'} + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -10061,6 +10309,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.1.0: + resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -10100,10 +10352,26 @@ packages: shiki@3.1.0: resolution: {integrity: sha512-LdTNyWQlC5zdCaHdcp1zPA1OVA2ivb+KjGOOnGcy02tGaF5ja+dGibWFH7Ar8YlngUgK/scDqworK18Ys9cbYA==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -10658,6 +10926,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.0: + resolution: {integrity: sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==} + engines: {node: '>= 0.6'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -11526,8 +11798,8 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} - xsschema@0.1.0-beta.9: - resolution: {integrity: sha512-x8NFpT6DvT3al+WrdEvK6sOI1igbtYi7Kx11rCT4iuOfbH+q/l57nFYkaFcfiO6cHhPuCMzCIQ3L7pICHbn++Q==} + xsschema@0.1.0: + resolution: {integrity: sha512-AZY7cxEBJHq4YioVsRQnvcAdUQ5it6ZoqEUaY4TqcuKtY5VEsdx4pX0/XFiMyx0eWdpfGt3TQCPiV9TaQM3EAw==} peerDependencies: '@valibot/to-json-schema': ^1.0.0-rc.0 arktype: ^2.0.4 @@ -11714,6 +11986,18 @@ snapshots: '@antfu/utils@8.1.0': {} + '@anthropic-ai/sdk@0.27.3(encoding@0.1.13)': + dependencies: + '@types/node': 18.19.76 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + '@apideck/better-ajv-errors@0.3.6(ajv@8.12.0)': dependencies: ajv: 8.12.0 @@ -12562,6 +12846,34 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@browserbasehq/sdk@2.3.0(encoding@0.1.13)': + dependencies: + '@types/node': 18.19.76 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@browserbasehq/stagehand@1.13.1(@playwright/test@1.50.1)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@16.4.7)(encoding@0.1.13)(openai@4.85.0(encoding@0.1.13)(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.24.2))(utf-8-validate@5.0.10)(zod@3.24.2)': + dependencies: + '@anthropic-ai/sdk': 0.27.3(encoding@0.1.13) + '@browserbasehq/sdk': 2.3.0(encoding@0.1.13) + '@playwright/test': 1.50.1 + deepmerge: 4.3.1 + dotenv: 16.4.7 + openai: 4.85.0(encoding@0.1.13)(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.24.2) + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + zod: 3.24.2 + zod-to-json-schema: 3.24.3(zod@3.24.2) + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -13709,6 +14021,20 @@ snapshots: - acorn - supports-color + '@modelcontextprotocol/sdk@1.6.1': + dependencies: + content-type: 1.0.5 + cors: 2.8.5 + eventsource: 3.0.5 + express: 5.0.1 + express-rate-limit: 7.5.0(express@5.0.1) + pkce-challenge: 4.1.0 + raw-body: 3.0.0 + zod: 3.24.2 + zod-to-json-schema: 3.24.3(zod@3.24.2) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.37.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14314,14 +14640,28 @@ snapshots: '@pkgr/core@0.1.0': {} + '@playwright/test@1.50.1': + dependencies: + playwright: 1.50.1 + '@polka/url@1.0.0-next.24': {} + '@proj-airi/server-sdk@0.1.4': + dependencies: + '@proj-airi/server-shared': 0.1.4 + crossws: 0.3.4 + defu: 6.1.4 + '@proj-airi/server-sdk@0.3.6': dependencies: '@proj-airi/server-shared': 0.3.6 crossws: 0.3.4 defu: 6.1.4 + '@proj-airi/server-shared@0.1.4': + dependencies: + crossws: 0.3.4 + '@proj-airi/server-shared@0.3.6': dependencies: crossws: 0.3.4 @@ -14357,12 +14697,14 @@ snapshots: optionalDependencies: rollup: 4.34.9 - '@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(rollup@2.79.1)': + '@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@2.79.1)': dependencies: '@babel/core': 7.26.0 '@babel/helper-module-imports': 7.25.9 '@rollup/pluginutils': 3.1.0(rollup@2.79.1) rollup: 2.79.1 + optionalDependencies: + '@types/babel__core': 7.20.5 transitivePeerDependencies: - supports-color @@ -14614,7 +14956,7 @@ snapshots: '@surma/rollup-plugin-off-main-thread@2.2.3': dependencies: - ejs: 3.1.9 + ejs: 3.1.10 json5: 2.2.3 magic-string: 0.25.9 string.prototype.matchall: 4.0.8 @@ -14695,6 +15037,31 @@ snapshots: '@types/audioworklet@0.0.71': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.7 + '@babel/types': 7.26.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + optional: true + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.7 + optional: true + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.7 + '@babel/types': 7.26.7 + optional: true + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.7 + optional: true + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 @@ -15011,7 +15378,7 @@ snapshots: dependencies: '@typeschema/core': 0.14.0(@types/json-schema@7.0.15) optionalDependencies: - '@typeschema/valibot': 0.14.0(@gcornut/valibot-json-schema@0.42.0(esbuild@0.25.0)(typescript@5.8.2))(@types/json-schema@7.0.15)(valibot@1.0.0-beta.9(typescript@5.8.2)) + '@typeschema/valibot': 0.14.0(@gcornut/valibot-json-schema@0.42.0(esbuild@0.19.12)(typescript@5.8.2))(@types/json-schema@7.0.15)(valibot@1.0.0-beta.9(typescript@5.8.2)) '@typeschema/zod': 0.14.0(@types/json-schema@7.0.15)(zod-to-json-schema@3.24.3(zod@3.24.2))(zod@3.24.2) transitivePeerDependencies: - '@types/json-schema' @@ -15941,47 +16308,53 @@ snapshots: '@xsai-ext/shared-providers@0.1.0-beta.9': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 '@xsai/embed@0.1.0-beta.9': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 '@xsai/generate-speech@0.1.0-beta.9': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 '@xsai/generate-text@0.1.0-beta.9': dependencies: - '@xsai/shared-chat': 0.1.0-beta.9 + '@xsai/shared-chat': 0.1.0 '@xsai/generate-transcription@0.1.0-beta.9': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 '@xsai/model@0.1.0-beta.9': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 '@xsai/providers@0.1.0-beta.5': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 + + '@xsai/shared-chat@0.1.0': + dependencies: + '@xsai/shared': 0.1.0 '@xsai/shared-chat@0.1.0-beta.9': dependencies: - '@xsai/shared': 0.1.0-beta.9 + '@xsai/shared': 0.1.0 + + '@xsai/shared@0.1.0': {} '@xsai/shared@0.1.0-beta.9': {} '@xsai/stream-text@0.1.0-beta.9': dependencies: - '@xsai/shared-chat': 0.1.0-beta.9 + '@xsai/shared-chat': 0.1.0 '@xsai/tool@0.1.0-beta.9(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)))(zod-to-json-schema@3.24.3(zod@3.24.2))': dependencies: - '@xsai/shared': 0.1.0-beta.9 - '@xsai/shared-chat': 0.1.0-beta.9 - xsschema: 0.1.0-beta.9(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)))(zod-to-json-schema@3.24.3(zod@3.24.2)) + '@xsai/shared': 0.1.0 + '@xsai/shared-chat': 0.1.0 + xsschema: 0.1.0(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)))(zod-to-json-schema@3.24.3(zod@3.24.2)) transitivePeerDependencies: - '@valibot/to-json-schema' - arktype @@ -15989,7 +16362,7 @@ snapshots: '@xsai/utils-chat@0.1.0-beta.9': dependencies: - '@xsai/shared-chat': 0.1.0-beta.9 + '@xsai/shared-chat': 0.1.0 abbrev@1.1.1: optional: true @@ -16003,6 +16376,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.0 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -16235,7 +16613,7 @@ snapshots: call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.2 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 @@ -16491,6 +16869,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.1.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.5.2 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.0 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} boolean@3.2.0: @@ -16666,6 +17058,11 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -16674,6 +17071,11 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camel-case@4.1.2: @@ -16984,6 +17386,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.5: {} convert-gitmoji@0.1.5: {} @@ -16994,6 +17400,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@0.7.2: {} @@ -17312,6 +17720,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.6: + dependencies: + ms: 2.1.2 + debug@4.3.7: dependencies: ms: 2.1.3 @@ -17522,6 +17934,12 @@ snapshots: dset@3.1.4: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} earcut@2.2.4: {} @@ -17541,6 +17959,10 @@ snapshots: ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.8.6 + ejs@3.1.9: dependencies: jake: 10.8.6 @@ -17715,14 +18137,14 @@ snapshots: es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 get-symbol-description: 1.0.0 globalthis: 1.0.3 - gopd: 1.0.1 + gopd: 1.2.0 has: 1.0.3 has-property-descriptors: 1.0.2 has-proto: 1.0.1 - has-symbols: 1.0.3 + has-symbols: 1.1.0 internal-slot: 1.0.5 is-array-buffer: 3.0.2 is-callable: 1.2.7 @@ -17752,13 +18174,19 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-module-lexer@1.6.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.1: dependencies: - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 has: 1.0.3 has-tostringtag: 1.0.0 @@ -18345,6 +18773,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.0: {} + + eventsource@3.0.5: + dependencies: + eventsource-parser: 3.0.0 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -18386,6 +18820,10 @@ snapshots: expect-type@1.1.0: {} + express-rate-limit@7.5.0(express@5.0.1): + dependencies: + express: 5.0.1 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -18422,6 +18860,43 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.0.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.1.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.3.6 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.0.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + methods: 1.1.2 + mime-types: 3.0.0 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + router: 2.1.0 + safe-buffer: 5.2.1 + send: 1.1.0 + serve-static: 2.1.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 2.0.0 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + expressive-code@0.40.1: dependencies: '@expressive-code/core': 0.40.1 @@ -18540,6 +19015,18 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.0.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + find-cache-dir@3.3.2: dependencies: commondir: 1.0.1 @@ -18624,6 +19111,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -18712,10 +19201,28 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.0 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-own-enumerable-property-symbols@3.0.2: {} get-port-please@3.1.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@5.2.0: dependencies: pump: 3.0.2 @@ -18732,7 +19239,7 @@ snapshots: get-symbol-description@1.0.0: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 get-tsconfig@4.8.1: dependencies: @@ -18852,6 +19359,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + got@11.8.6: dependencies: '@sindresorhus/is': 4.6.0 @@ -18919,9 +19428,11 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.0: dependencies: - has-symbols: 1.0.3 + has-symbols: 1.1.0 has-unicode@2.0.1: optional: true @@ -18937,6 +19448,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -19248,6 +19763,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.5.2: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -19297,9 +19816,9 @@ snapshots: internal-slot@1.0.5: dependencies: - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 has: 1.0.3 - side-channel: 1.0.6 + side-channel: 1.1.0 internmap@2.0.3: {} @@ -19322,7 +19841,7 @@ snapshots: is-array-buffer@3.0.2: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-typed-array: 1.1.12 is-arrayish@0.3.2: {} @@ -19405,6 +19924,8 @@ snapshots: is-potential-custom-element-name@1.0.1: optional: true + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.6 @@ -19436,7 +19957,7 @@ snapshots: is-symbol@1.0.4: dependencies: - has-symbols: 1.0.3 + has-symbols: 1.1.0 is-typed-array@1.1.12: dependencies: @@ -19922,6 +20443,8 @@ snapshots: escape-string-regexp: 4.0.0 optional: true + math-intrinsics@1.1.0: {} + mdast-util-definitions@6.0.0: dependencies: '@types/mdast': 4.0.4 @@ -20115,8 +20638,12 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -20408,10 +20935,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.53.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.0: + dependencies: + mime-db: 1.53.0 + mime@1.6.0: {} mime@2.6.0: {} @@ -20647,6 +21180,8 @@ snapshots: ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} msw@2.7.3(@types/node@22.13.8)(typescript@5.8.2): @@ -20703,6 +21238,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + neotraverse@0.6.18: {} neuri@0.0.22(@types/json-schema@7.0.15)(@typeschema/valibot@0.14.0(@types/json-schema@7.0.15)(valibot@1.0.0-beta.9(typescript@5.8.2)))(@typeschema/zod@0.14.0(@types/json-schema@7.0.15)(zod-to-json-schema@3.24.3(zod@3.24.2))(zod@3.24.2))(encoding@0.1.13)(vitest@3.0.7)(ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod-to-json-schema@3.24.3(zod@3.24.2))(zod@3.24.2): @@ -20872,7 +21409,7 @@ snapshots: dependencies: call-bind: 1.0.7 define-properties: 1.2.0 - has-symbols: 1.0.3 + has-symbols: 1.1.0 object-keys: 1.1.1 obuf@1.1.2: {} @@ -21148,6 +21685,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} + path-type@5.0.0: optional: true @@ -21286,6 +21825,8 @@ snapshots: '@pixi/utils': 6.5.10(@pixi/constants@6.5.10)(@pixi/settings@6.5.10(@pixi/constants@6.5.10)) gh-pages: 4.0.0 + pkce-challenge@4.1.0: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -21787,6 +22328,10 @@ snapshots: dependencies: side-channel: 1.0.6 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.6: {} querystringify@2.2.0: {} @@ -21834,6 +22379,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -22261,6 +22813,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.9 fsevents: 2.3.3 + router@2.1.0: + dependencies: + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + rrweb-cssom@0.7.1: optional: true @@ -22280,8 +22838,8 @@ snapshots: safe-array-concat@1.0.1: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 isarray: 2.0.5 safe-buffer@5.1.2: {} @@ -22291,7 +22849,7 @@ snapshots: safe-regex-test@1.0.0: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-regex: 1.1.4 safer-buffer@2.1.2: {} @@ -22348,6 +22906,23 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.1.0: + dependencies: + debug: 4.4.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime-types: 2.1.35 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 @@ -22366,6 +22941,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.1.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.1.0 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: optional: true @@ -22444,6 +23028,26 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.3 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -22451,6 +23055,14 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.3 + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -22659,11 +23271,11 @@ snapshots: call-bind: 1.0.7 define-properties: 1.2.0 es-abstract: 1.22.2 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 internal-slot: 1.0.5 regexp.prototype.flags: 1.5.1 - side-channel: 1.0.6 + side-channel: 1.1.0 string.prototype.trim@1.2.8: dependencies: @@ -23064,12 +23676,18 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.0: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.0 + type@2.7.3: {} typed-array-buffer@1.0.0: dependencies: call-bind: 1.0.7 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-typed-array: 1.1.12 typed-array-byte-length@1.0.0: @@ -23128,7 +23746,7 @@ snapshots: dependencies: call-bind: 1.0.7 has-bigints: 1.0.2 - has-symbols: 1.0.3 + has-symbols: 1.1.0 which-boxed-primitive: 1.0.2 unbuild@3.0.0-rc.11(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2)): @@ -23402,17 +24020,17 @@ snapshots: '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.34.9) '@vueuse/core': 12.7.0(typescript@5.8.2) - unplugin-combine@1.2.0(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)): + unplugin-combine@1.2.0(esbuild@0.19.12)(rollup@4.34.9)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)): optionalDependencies: esbuild: 0.19.12 - rollup: 2.79.1 + rollup: 4.34.9 unplugin: 1.16.1 vite: 6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) - unplugin-combine@1.2.0(esbuild@0.25.0)(rollup@4.34.9)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)): + unplugin-combine@1.2.0(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)): optionalDependencies: esbuild: 0.25.0 - rollup: 4.34.9 + rollup: 2.79.1 unplugin: 1.16.1 vite: 6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) @@ -23463,7 +24081,7 @@ snapshots: transitivePeerDependencies: - vue - unplugin-vue-macros@2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)): + unplugin-vue-macros@2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.19.12)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)): dependencies: '@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.2)) '@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.2)) @@ -23493,9 +24111,9 @@ snapshots: '@vue-macros/short-bind': 1.1.5(vue@3.5.13(typescript@5.8.2)) '@vue-macros/short-emits': 1.6.5(vue@3.5.13(typescript@5.8.2)) '@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.2)) - '@vue-macros/volar': 0.30.15(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) + '@vue-macros/volar': 0.30.15(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) unplugin: 1.16.1 - unplugin-combine: 1.2.0(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) + unplugin-combine: 1.2.0(esbuild@0.19.12)(rollup@4.34.9)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.2)) vue: 3.5.13(typescript@5.8.2) transitivePeerDependencies: @@ -23509,7 +24127,7 @@ snapshots: - vue-tsc - webpack - unplugin-vue-macros@2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.25.0)(rollup@4.34.9)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)): + unplugin-vue-macros@2.14.5(@vueuse/core@12.7.0(typescript@5.8.2))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)): dependencies: '@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.2)) '@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.2)) @@ -23539,9 +24157,9 @@ snapshots: '@vue-macros/short-bind': 1.1.5(vue@3.5.13(typescript@5.8.2)) '@vue-macros/short-emits': 1.6.5(vue@3.5.13(typescript@5.8.2)) '@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.2)) - '@vue-macros/volar': 0.30.15(rollup@4.34.9)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) + '@vue-macros/volar': 0.30.15(rollup@2.79.1)(typescript@5.8.2)(vue-tsc@2.2.6(typescript@5.8.2))(vue@3.5.13(typescript@5.8.2)) unplugin: 1.16.1 - unplugin-combine: 1.2.0(esbuild@0.25.0)(rollup@4.34.9)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) + unplugin-combine: 1.2.0(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)) unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.2)) vue: 3.5.13(typescript@5.8.2) transitivePeerDependencies: @@ -23850,13 +24468,13 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-pwa@0.21.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0)(workbox-window@7.3.0): + vite-plugin-pwa@0.21.1(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): dependencies: debug: 4.4.0 pretty-bytes: 6.1.1 tinyglobby: 0.2.12 vite: 6.2.0(@types/node@22.13.8)(jiti@2.4.2)(less@4.2.1)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0) - workbox-build: 7.3.0 + workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.3.0 transitivePeerDependencies: - supports-color @@ -24103,7 +24721,7 @@ snapshots: available-typed-arrays: 1.0.5 call-bind: 1.0.7 for-each: 0.3.3 - gopd: 1.0.1 + gopd: 1.2.0 has-tostringtag: 1.0.0 which@2.0.2: @@ -24139,13 +24757,13 @@ snapshots: dependencies: workbox-core: 7.3.0 - workbox-build@7.3.0: + workbox-build@7.3.0(@types/babel__core@7.20.5): dependencies: '@apideck/better-ajv-errors': 0.3.6(ajv@8.12.0) '@babel/core': 7.26.0 '@babel/preset-env': 7.26.0(@babel/core@7.26.0) '@babel/runtime': 7.26.7 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(rollup@2.79.1) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@2.79.1) '@rollup/plugin-node-resolve': 15.3.0(rollup@2.79.1) '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) '@rollup/plugin-terser': 0.4.4(rollup@2.79.1) @@ -24297,7 +24915,7 @@ snapshots: xmlhttprequest-ssl@2.1.2: {} - xsschema@0.1.0-beta.9(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)))(zod-to-json-schema@3.24.3(zod@3.24.2)): + xsschema@0.1.0(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)))(zod-to-json-schema@3.24.3(zod@3.24.2)): optionalDependencies: '@valibot/to-json-schema': 1.0.0-rc.0(valibot@1.0.0-beta.9(typescript@5.8.2)) zod-to-json-schema: 3.24.3(zod@3.24.2) diff --git a/services/twitter-services/.env.example b/services/twitter-services/.env.example new file mode 100644 index 00000000..9d652ead --- /dev/null +++ b/services/twitter-services/.env.example @@ -0,0 +1,21 @@ +# Browser Config +BROWSER_HEADLESS=false +BROWSER_USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 +BROWSER_VIEWPORT_WIDTH=1280 +BROWSER_VIEWPORT_HEIGHT=800 +BROWSER_TIMEOUT=30000 +BROWSER_REQUEST_TIMEOUT=20000 +BROWSER_REQUEST_RETRIES=2 + +# Adapter Config +ENABLE_AIRI=false +AIRI_URL=http://localhost:3000 +AIRI_TOKEN=your_airi_token + +ENABLE_MCP=true +MCP_PORT=8080 + +# System Config +LOG_LEVEL=info # Optional: error, warn, info, verbose, debug +LOG_FORMAT=pretty # Optional: json, pretty +CONCURRENCY=1 diff --git a/services/twitter-services/docs/architecture.md b/services/twitter-services/docs/architecture.md new file mode 100644 index 00000000..778c6687 --- /dev/null +++ b/services/twitter-services/docs/architecture.md @@ -0,0 +1,396 @@ +# Twitter Service Architecture Documentation + +## 1. Project Overview + +Twitter Service is a web automation service based on BrowserBase, providing structured access and interaction capabilities with Twitter data. It employs a layered architecture design that supports multiple adapters for integration with different applications. + +## 2. Design Goals + +- **Reliability**: Stable handling of Twitter page changes and limitations +- **Scalability**: Easy to add new features and support different integration methods +- **Performance Optimization**: Intelligent management of request frequency and browser sessions +- **Data Structuring**: Provides standardized, typed data models + +## 3. Architecture Overview + +``` +┌─────────────────────────────────────────────┐ +│ Application/Consumer Layer │ +│ │ +│ ┌────────────┐ ┌─────────────┐ │ +│ │ │ │ │ │ +│ │ Airi Core │ │ Other LLM │ │ +│ │ │ │ Applications│ │ +│ │ │ │ │ │ +│ └──────┬─────┘ └──────┬──────┘ │ +└──────────┼─────────────────────┼────────────┘ + │ │ +┌──────────▼─────────────────────▼────────────┐ +│ Adapter Layer │ +│ │ +│ ┌────────────┐ ┌─────────────┐ │ +│ │Airi Adapter│ │ MCP Adapter │ │ +│ │(@server-sdk)│ │ (HTTP/JSON) │ │ +│ └──────┬─────┘ └──────┬──────┘ │ +└──────────┼─────────────────────┼────────────┘ + │ │ +┌──────────▼─────────────────────▼────────────┐ +│ Core Services Layer │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ Twitter Services │ │ +│ │ │ │ +│ │ ┌────────┐ ┌────────────┐ │ │ +│ │ │ Auth │ │ Timeline │ │ │ +│ │ │ Service│ │ Service │ │ │ +│ │ └────────┘ └────────────┘ │ │ +│ │ │ │ +│ └──────────────────┬───────────────┘ │ +└──────────────────────┼──────────────────────┘ + │ + ┌───────────▼────────────┐ + │ Browser Adapter Layer │ + │ (BrowserAdapter) │ + └───────────┬────────────┘ + │ + ┌───────────▼────────────┐ + │ Stagehand │ + └───────────┬────────────┘ + │ + ┌───────────▼────────────┐ + │ Playwright │ + └────────────────────────┘ +``` + +## 4. Technology Stack and Dependencies + +- **Core Library**: TypeScript, Node.js +- **Browser Automation**: BrowserBase Stagehand, Playwright +- **HTML Parsing**: unified, rehype-parse, unist-util-visit +- **API Server**: H3.js, listhen +- **Adapters**: Airi Server SDK, MCP SDK +- **Logging System**: @guiiai/logg +- **Configuration**: defu (deep merging configurations) +- **Utility Library**: zod (type validation) + +## 5. Key Components + +### 5.1 Adapter Layer + +#### 5.1.1 Airi Adapter + +Provides integration with the Airi LLM platform, handling event-driven communication. + +#### 5.1.2 MCP Adapter + +Implements the Model Context Protocol interface, providing communication based on HTTP. Currently using the official MCP SDK implementation, providing high-performance HTTP server and SSE communication through H3.js. + +The MCP adapter exposes several tools and resources: + +- **Timeline Resource**: Access tweets from the user's timeline +- **Tweet Details Resource**: Get detailed information about a specific tweet +- **User Profile Resource**: Retrieve user profile information + +Additionally, it provides tools for interaction: + +- **Login Tool**: Simplified authentication tool that provides clear feedback on session status. It attempts to load existing sessions, and clearly communicates whether a session was loaded successfully or if manual login is required. The tool no longer requires username/password parameters, as it relies on the enhanced session management system. +- **Post Tweet Tool**: Create and publish new tweets +- **Like Tweet Tool**: Like a tweet by its ID +- **Retweet Tool**: Retweet a tweet by its ID +- **Refresh Timeline Tool**: Refresh the timeline with the latest tweets, with options to control the count and whether to include replies and retweets. +- **Get My Profile Tool**: Get information about a user's profile. It can extract the username from the current URL or accept a specific username as a parameter. + +The adapter uses internationalized messages (Chinese/English) to provide clear feedback to users about login status and session management. + +#### 5.1.3 Development Server + +Using listhen for optimized development experience, including automatic browser opening, real-time logging, and debugging tools. + +### 5.2 Core Service Layer + +#### 5.2.1 Authentication Service (Auth Service) + +The Authentication Service has been significantly enhanced to improve reliability and error handling: + +1. **Improved Session Detection**: Enhanced logic for detecting existing browser sessions +2. **Robust Error Handling**: Implemented granular error handling to distinguish between different authentication failure types +3. **Timeout Optimization**: Adjusted timeouts for various operations to enhance stability during network fluctuations +4. **Enhanced Cookie Management**: Improved cookie storage and loading mechanisms to reduce the need for manual login +5. **Session Validation**: Added comprehensive session validation to verify the integrity of saved sessions +6. **Simplified API**: Removed the need for explicit username/password in the login method, relying instead on session files and browser session detection + +The service follows a multi-stage authentication approach: + +1. **Session File Loading**: First attempts to load saved sessions from disk +2. **Existing Session Detection**: Checks if the browser already has a valid Twitter session +3. **Manual Login Process**: If necessary, guides through the Twitter login page + +After successful authentication through any method, sessions are automatically persisted for future use. The system provides clear feedback to users about the current login state and automatically monitors and saves sessions when changes are detected. + +#### 5.2.2 Timeline Service (Timeline Service) + +Gets and processes Twitter timeline content. + +#### 5.2.3 Other Services + +Includes search service, interaction service, user profile service, etc. (not implemented in MVP) + +### 5.3 Parsers and Tools + +#### 5.3.1 Tweet Parser + +Extracts structured data from HTML. + +#### 5.3.2 Rate Limiter + +Controls request frequency to avoid triggering Twitter limits. + +#### 5.3.3 Session Manager + +Manages authentication session data, providing methods to: + +- Save session cookies to local files +- Load previous sessions during startup +- Delete invalid or expired sessions +- Validate session age and integrity + +### 5.3.4 Browser Adapter Layer + +The service has migrated from direct BrowserBase API usage to Stagehand, an AI-powered web browsing framework built on top of Playwright. Stagehand offers three core APIs that simplify browser automation: + +- **act**: Execute actions on the page through natural language instructions +- **extract**: Retrieve structured data from the page using natural language queries +- **observe**: Analyze the page and suggest possible actions before execution + +Stagehand processes the DOM in chunks to optimize LLM performance and provides fallback vision capabilities for complex page structures. This migration significantly improves code maintainability and automation reliability when interacting with Twitter's interface. + +## 6. Data Flow + +1. **Request Flow**: Application Layer → Adapter → Core Service → Browser Adapter Layer → BrowserBase API → Twitter +2. **Response Flow**: Twitter → BrowserBase API → Browser Adapter Layer → Core Service → Data Parsing → Adapter → Application Layer +3. **Authentication Flow**: + - Load Session → Check Existing Session → Manual Login → Session Validation → Session Storage + - Clear feedback is provided at each step of the authentication process + +## 7. Configuration System + +The configuration system has been optimized using the `defu` library for deep merging configurations, eliminating redundant initialization. The updated configuration structure includes Stagehand-specific settings: + +```typescript +interface Config { + // BrowserBase/Stagehand configuration + browserbase: { + apiKey: string + projectId?: string + endpoint?: string + stagehand?: { + modelName?: string // e.g., "gpt-4o" or "claude-3-5-sonnet-latest" + modelClientOptions?: { + apiKey: string // OpenAI or Anthropic API key + } + } + } + + // Browser configuration + browser: BrowserConfig + + // Twitter configuration + twitter: { + credentials?: TwitterCredentials + defaultOptions?: { + timeline?: TimelineOptions + search?: SearchOptions + } + } + + // Adapter configuration + adapters: { + airi?: { + url?: string + token?: string + enabled: boolean + } + mcp?: { + port?: number + enabled: boolean + } + } + + // System configuration + system: { + logLevel: string + concurrency: number + } +} +``` + +The system no longer relies on the `TWITTER_COOKIES` environment variable, as cookies are now managed through the session management system. + +## 8. Development and Testing + +### 8.1 Development Environment Setup + +```bash +# Install dependencies +npm install + +# Set environment variables +cp .env.example .env +# Edit .env to add BrowserBase API key and Twitter credentials (optional) + +# Development mode startup +npm run dev # Standard mode +npm run dev:mcp # MCP development server mode +``` + +### 8.2 Testing Strategy + +- **Unit Tests**: Test parsers, utility classes, and business logic +- **Integration Tests**: Test service and adapter interaction +- **End-to-End Tests**: Simulate complete usage scenarios + +## 9. Integration Example + +### 9.1 Integration Example with Stagehand + +```typescript +import { StagehandAdapter, TwitterService } from 'twitter-services' + +async function main() { + // Initialize Stagehand adapter + const browser = new StagehandAdapter(process.env.BROWSERBASE_API_KEY, process.env.BROWSERBASE_PROJECT_ID) + await browser.initialize({ + headless: true, + stagehand: { + modelName: 'gpt-4o', // Or 'claude-3-5-sonnet-latest' for Anthropic + modelClientOptions: { + apiKey: process.env.OPENAI_API_KEY // Or process.env.ANTHROPIC_API_KEY + } + } + }) + + // Create Twitter service + const twitter = new TwitterService(browser) + + // Authenticate - will try multi-stage approach + const loggedIn = await twitter.login() + + if (loggedIn) { + console.log('Login successful') + + // Get timeline using natural language capabilities of Stagehand + const tweets = await twitter.getTimeline({ count: 10 }) + console.log(tweets) + } + else { + console.error('Login failed') + } + + // Release resources + await browser.close() +} +``` + +### 9.2 Integrating as Airi Module + +```typescript +import { AiriAdapter, BrowserBaseMCPAdapter, TwitterService } from 'twitter-services' + +async function startAiriModule() { + const browser = new BrowserBaseMCPAdapter(process.env.BROWSERBASE_API_KEY) + await browser.initialize({ headless: true }) + + const twitter = new TwitterService(browser) + + // Create Airi adapter + const airiAdapter = new AiriAdapter(twitter, { + url: process.env.AIRI_URL, + token: process.env.AIRI_TOKEN + }) + + // Start adapter + await airiAdapter.start() + + console.log('Twitter service running as Airi module') +} +``` + +### 9.3 Using MCP for Integration + +```typescript +// Use MCP SDK to interact with Twitter service +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' + +async function connectToTwitterService() { + // Create SSE transport + const transport = new SSEClientTransport('http://localhost:8080/sse', 'http://localhost:8080/messages') + + // Create client + const client = new Client() + await client.connect(transport) + + // Get timeline + const timeline = await client.get('twitter://timeline/10') + console.log('Timeline:', timeline.contents) + + // Use simplified login tool without parameters + const loginResult = await client.useTool('login', {}) + console.log('Login result:', loginResult.content[0].text) + + // Use refresh timeline tool to get latest tweets + const refreshResult = await client.useTool('refresh-timeline', { count: 15, includeReplies: false }) + console.log('Refresh result:', refreshResult.content[0].text) + console.log('New tweets:', refreshResult.resources) + + // Get user profile information + const profileResult = await client.useTool('get-my-profile', { username: 'twitter' }) + console.log('Profile info:', profileResult.content[0].text) + + // Use tool to send tweet + const result = await client.useTool('post-tweet', { content: 'Hello from MCP!' }) + console.log('Result:', result.content) + + return client +} +``` + +## 10. Extension Guide + +### 10.1 Adding New Features + +For example, adding "Get Tweets from a Specific User" functionality: + +1. Extend the interface in `src/types/twitter.ts` +2. Implement the method in `src/core/twitter-service.ts` +3. Add corresponding handling logic in the adapter +4. If it's an MCP adapter, add appropriate resources or tools in `configureServer()` + +### 10.2 Supporting New Adapters + +1. Create a new adapter class +2. Implement communication logic with the target system +3. Add configuration support in the entry file + +## 11. Maintenance Recommendations + +- **Automated Testing**: Write unit tests and integration tests +- **Monitoring & Alerts**: Monitor service status and Twitter access limitations +- **Selector Updates**: Regularly validate and update selector configurations +- **Session Management**: Use the built-in session management system to improve stability and reduce manual login requirements. Consider implementing session rotation and validation. +- **Cookie Management**: The system now automatically manages cookie storage via the SessionManager, but consider adding encrypted storage for production environments. +- **User Feedback**: Maintain clear, internationalized feedback messages for authentication status to improve user experience. + +### 11.4 Stagehand Maintenance + +- **Model Selection**: Regularly evaluate the performance of different LLM models (GPT-4o, Claude 3.5 Sonnet) for your specific use cases +- **Prompt Engineering**: Refine natural language instructions to improve reliability and performance +- **Vision Capabilities**: Consider enabling vision capabilities for complex DOM structures by setting `useVision: true` in appropriate operations +- **DOM Chunking**: Monitor and optimize chunk sizes based on the complexity of the Twitter interface + +## 12. Project Roadmap + +- MVP Stage: Core functionality with Stagehand integration (authentication, browsing timeline) +- Stage Two: Enhanced interaction features utilizing Stagehand's natural language capabilities +- Stage Three: Advanced search and filtering features with optimized LLM prompts +- Stage Four: Performance optimization and multi-model support diff --git a/services/twitter-services/package.json b/services/twitter-services/package.json new file mode 100644 index 00000000..00892d42 --- /dev/null +++ b/services/twitter-services/package.json @@ -0,0 +1,31 @@ +{ + "name": "@proj-airi/twitter-services", + "type": "module", + "version": "0.1.0", + "description": "Twitter Services for MCP", + "author": "RainbowBird ", + "license": "MIT", + "scripts": { + "dev": "tsx src/main.ts", + "mcp:ui": "pnpx @modelcontextprotocol/inspector", + "postinstall": "playwright install chromium" + }, + "dependencies": { + "@browserbasehq/stagehand": "^1.13.1", + "@guiiai/logg": "^1.0.0", + "@modelcontextprotocol/sdk": "^1.6.1", + "@proj-airi/server-sdk": "^0.1.0", + "defu": "^6.1.4", + "dotenv": "^16.4.7", + "h3": "^1.11.0", + "listhen": "^1.6.0", + "ofetch": "^1.3.3", + "playwright": "^1.50.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^18.16.3", + "tsx": "^4.19.0", + "typescript": "^5.0.4" + } +} diff --git a/services/twitter-services/src/adapters/airi-adapter.ts b/services/twitter-services/src/adapters/airi-adapter.ts new file mode 100644 index 00000000..c4099e97 --- /dev/null +++ b/services/twitter-services/src/adapters/airi-adapter.ts @@ -0,0 +1,5 @@ +/** + * Airi Adapter + * Adapts the Twitter service as an Airi module + */ +export class AiriAdapter {} diff --git a/services/twitter-services/src/adapters/mcp-adapter.ts b/services/twitter-services/src/adapters/mcp-adapter.ts new file mode 100644 index 00000000..c1fb4e9a --- /dev/null +++ b/services/twitter-services/src/adapters/mcp-adapter.ts @@ -0,0 +1,577 @@ +import type { TwitterService } from '../core/twitter-service' + +import { Buffer } from 'node:buffer' +import { createServer } from 'node:http' +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' +import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3' +import { z } from 'zod' + +import { errorToMessage } from '../utils/error' +import { logger } from '../utils/logger' + +/** + * MCP Protocol Adapter + * Adapts the Twitter service to MCP protocol using official MCP SDK + * Implements HTTP server using H3.js + */ +export class MCPAdapter { + private twitterService: TwitterService + private mcpServer: McpServer + private app: ReturnType + private server: ReturnType | null = null + private port: number + private activeTransports: SSEServerTransport[] = [] + private extraResourceInfo: string[] = [] + + constructor(twitterService: TwitterService, port: number = 8080) { + this.twitterService = twitterService + this.port = port + + // Create MCP server + this.mcpServer = new McpServer({ + name: 'Twitter Service', + version: '1.0.0', + }) + + // Create H3 app + this.app = createApp() + + // Configure resources and tools + this.configureServer() + + // Set up H3 routes + this.setupRoutes() + } + + /** + * Configure MCP server resources and tools + */ + private configureServer(): void { + logger.mcp.debug('Configuring MCP server resources and tools...') + + // Add timeline resource with improved registration + this.mcpServer.resource( + 'timeline', + new ResourceTemplate('twitter://timeline/{count}', { + list: async () => { + logger.mcp.debug('Listing available timeline resources') + return { + resources: [{ + name: 'timeline', + uri: 'twitter://timeline/10', // Default number of tweets + description: 'Tweet timeline', + }], + } + }, + }), + async (uri: URL, { count }: { count?: string }) => { + try { + logger.mcp.withField('uri', uri.toString()).withField('count', count || 'default').debug('Getting timeline') + + const tweets = await this.twitterService.getTimeline({ + count: count ? Number.parseInt(count) : undefined, + }) + + logger.mcp.withField('tweetCount', tweets.length).debug('Successfully retrieved timeline tweets') + + return { + contents: tweets.map(tweet => ({ + uri: `twitter://tweet/${tweet.id}`, + text: `Tweet by @${tweet.author.username} (${tweet.author.displayName}):\n${tweet.text}`, + })), + } + } + catch (error) { + logger.mcp.errorWithError('Failed to get timeline:', error) + return { contents: [] } + } + }, + ) + + // Add tweet details resource + this.mcpServer.resource( + 'tweet', + new ResourceTemplate('twitter://tweet/{id}', { list: undefined }), + async (uri: URL, { id }) => { + try { + const tweet = await this.twitterService.getTweetDetails(id as string) + + return { + contents: [{ + uri: uri.href, + text: `Tweet by @${tweet.author.username} (${tweet.author.displayName}):\n${tweet.text}`, + }], + } + } + catch (error) { + logger.mcp.errorWithError('Error fetching tweet details:', error) + return { contents: [] } + } + }, + ) + + // Add user profile resource + this.mcpServer.resource( + 'profile', + new ResourceTemplate('twitter://user/{username}', { list: undefined }), + async (uri, { username }) => { + try { + const profile = await this.twitterService.getUserProfile(username as string) + + return { + contents: [{ + uri: uri.href, + text: `Profile for @${profile.username} (${profile.displayName})\n${profile.bio || ''}`, + }], + } + } + catch (error) { + logger.mcp.errorWithError('Error fetching user profile:', error) + return { contents: [] } + } + }, + ) + + // Add login tool + this.mcpServer.tool( + 'login', + {}, + async () => { + try { + const success = await this.twitterService.login() + + return { + content: [{ + type: 'text', + text: success + ? 'Successfully loaded login state from session file! If you logged in manually, auto-monitoring is set up to save your session.' + : 'No valid session file found. Please log in manually in the browser, the system will automatically save your session.', + }], + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to check login status: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add post tweet tool + this.mcpServer.tool( + 'post-tweet', + { + content: z.string(), + replyTo: z.string().optional(), + media: z.array(z.string()).optional(), + }, + async ({ content, replyTo, media }) => { + try { + const tweetId = await this.twitterService.postTweet(content, { + inReplyTo: replyTo, + media, + }) + + return { + content: [{ + type: 'text', + text: `Successfully posted tweet: ${tweetId}`, + }], + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to post tweet: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add like tweet tool + this.mcpServer.tool( + 'like-tweet', + { tweetId: z.string() }, + async ({ tweetId }) => { + try { + const success = await this.twitterService.likeTweet(tweetId) + + return { + content: [{ + type: 'text', + text: success ? 'Successfully liked tweet' : 'Failed to like tweet', + }], + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to like tweet: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add retweet tool + this.mcpServer.tool( + 'retweet', + { tweetId: z.string() }, + async ({ tweetId }) => { + try { + const success = await this.twitterService.retweet(tweetId) + + return { + content: [{ + type: 'text', + text: success ? 'Successfully retweeted' : 'Failed to retweet', + }], + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to retweet: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add save session tool + this.mcpServer.tool( + 'save-session', + {}, + async () => { + try { + const success = await this.twitterService.saveSession() + + return { + content: [{ + type: 'text', + text: success + ? 'Successfully saved browser session to file. This session will be loaded automatically next time.' + : 'Failed to save browser session', + }], + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to save session: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add search tool + this.mcpServer.tool( + 'search', + { + query: z.string(), + count: z.number().optional(), + filter: z.enum(['latest', 'photos', 'videos', 'top']).optional(), + }, + async ({ query, count, filter }) => { + try { + const results = await this.twitterService.searchTweets(query, { count, filter }) + + return { + content: [{ + type: 'text', + text: `Search results: ${results.length} tweets`, + }], + resources: results.map(tweet => `twitter://tweet/${tweet.id}`), + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Search failed: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add refresh timeline tool + this.mcpServer.tool( + 'refresh-timeline', + { + count: z.number().optional(), + includeReplies: z.boolean().optional(), + includeRetweets: z.boolean().optional(), + }, + async ({ count, includeReplies, includeRetweets }) => { + try { + const tweets = await this.twitterService.getTimeline({ + count, + includeReplies, + includeRetweets, + }) + + return { + content: [{ + type: 'text', + text: `Successfully refreshed timeline, retrieved ${tweets.length} tweets`, + }], + resources: tweets.map(tweet => `twitter://tweet/${tweet.id}`), + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to refresh timeline: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + + // Add get my profile tool + this.mcpServer.tool( + 'get-my-profile', + { + username: z.string().optional(), + }, + async ({ username }) => { + try { + let profileUsername = username + + // If no username provided, try to get from current URL + if (!profileUsername) { + const currentUrl = await this.twitterService.getCurrentUrl() + profileUsername = this.extractUsernameFromUrl(currentUrl) + } + + // If we still don't have a username, return an error + if (!profileUsername) { + return { + content: [{ + type: 'text', + text: `Failed to get profile: Please provide a username or navigate to a profile page`, + }], + isError: true, + } + } + + const profile = await this.twitterService.getUserProfile(profileUsername) + + return { + content: [{ + type: 'text', + text: `Profile Information:\n` + + `Username: @${profile.username}\n` + + `Display Name: ${profile.displayName}\n` + + `Bio: ${profile.bio || 'Not set'}\n` + + `Followers: ${profile.followersCount || 'N/A'}\n` + + `Following: ${profile.followingCount || 'N/A'}\n` + + `Tweets: ${profile.tweetCount || 'N/A'}\n` + + `Joined: ${profile.joinDate || 'N/A'}`, + }], + resources: [`twitter://user/${profile.username}`], + } + } + catch (error) { + return { + content: [{ type: 'text', text: `Failed to get profile: ${errorToMessage(error)}` }], + isError: true, + } + } + }, + ) + } + + /** + * Extract username from Twitter URL + * @param url Twitter URL + * @returns Username or undefined if not a profile URL + */ + private extractUsernameFromUrl(url: string): string | undefined { + try { + const parsedUrl = new URL(url) + if (parsedUrl.hostname === 'x.com') { + const pathParts = parsedUrl.pathname.split('/').filter(Boolean) + if (pathParts.length > 0 && !['search', 'explore', 'home', 'notifications', 'messages'].includes(pathParts[0])) { + return pathParts[0] + } + } + return undefined + } + catch (e) { + logger.mcp.errorWithError('Error extracting username from URL:', e) + return undefined + } + } + + /** + * Set up H3 routes + */ + private setupRoutes(): void { + const router = createRouter() + + // Set up CORS + router.use('*', defineEventHandler((event) => { + event.node.res.setHeader('Access-Control-Allow-Origin', '*') + event.node.res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + event.node.res.setHeader('Access-Control-Allow-Headers', 'Content-Type') + + if (event.node.req.method === 'OPTIONS') { + event.node.res.statusCode = 204 + event.node.res.end() + } + })) + + // SSE endpoint + router.get('/sse', defineEventHandler(async (event) => { + const { req, res } = event.node + + res.setHeader('Content-Type', 'text/event-stream') + res.setHeader('Cache-Control', 'no-cache') + res.setHeader('Connection', 'keep-alive') + + // Create SSE transport + const transport = new SSEServerTransport('/messages', res) + this.activeTransports.push(transport) + + // Clean up when client disconnects + req.on('close', () => { + const index = this.activeTransports.indexOf(transport) + if (index !== -1) { + this.activeTransports.splice(index, 1) + } + }) + + // Connect to MCP server + await this.mcpServer.connect(transport) + })) + + // Messages endpoint - receive client requests + router.post('/messages', defineEventHandler(async (event) => { + if (this.activeTransports.length === 0) { + logger.mcp.warn('Received message request but no active SSE connections') + event.node.res.statusCode = 503 + return { error: 'No active SSE connections' } + } + + try { + // Parse request body + const body = await readBody(event) + logger.mcp.debug(`Received MCP request: ${JSON.stringify(body)}`) + + // Simple handling - send to most recent transport + // Note: In production, should use session ID to route to correct transport + const transport = this.activeTransports[this.activeTransports.length - 1] + + // Manually handle POST message, as H3 is not Express-compatible + const response = await transport.handleMessage(body) + + // Log response for debugging + logger.mcp.debug(`MCP response: ${JSON.stringify(response)}`) + + return response + } + catch (error) { + logger.mcp.errorWithError('Error handling MCP message:', error) + event.node.res.statusCode = 500 + return { error: errorToMessage(error) } + } + })) + + // Root path - provide service info + router.get('/', defineEventHandler(() => { + return { + name: 'Twitter MCP Service', + version: '1.0.0', + endpoints: { + sse: '/sse', + messages: '/messages', + }, + } + })) + + // Use router + this.app.use(router) + } + + /** + * Start MCP server + */ + start(): Promise { + return new Promise((resolve, reject) => { + if (this.server !== null) { + logger.mcp.warn('MCP server is already running') + resolve() + return + } + + try { + // Create Node.js HTTP server + this.server = createServer(toNodeListener(this.app)) + + // Add error event handlers + this.server.on('error', (error) => { + logger.mcp.errorWithError('MCP server error:', error) + reject(error) + }) + + // Log available resources for debugging + logger.mcp.debug('Registered MCP resources:') + logger.mcp.debug('- twitter://timeline/{count}: Get tweet timeline') + logger.mcp.debug('- twitter://tweet/{id}: Get single tweet details') + logger.mcp.debug('- twitter://user/{username}: Get user profile information') + if (this.extraResourceInfo?.length) { + this.extraResourceInfo.forEach((info) => { + logger.mcp.debug(`- ${info}`) + }) + } + + this.server.listen(this.port, () => { + const serverAddress = `http://localhost:${this.port}` + logger.mcp.log(`MCP server started at: ${serverAddress}`) + logger.mcp.log(`SSE endpoint: ${serverAddress}/sse`) + logger.mcp.log(`Messages endpoint: ${serverAddress}/messages`) + resolve() + }) + } + catch (error) { + logger.mcp.errorWithError('Error starting MCP server:', error) + reject(error) + } + }) + } + + /** + * Stop MCP server + */ + stop(): Promise { + return new Promise((resolve) => { + if (this.server === null) { + logger.mcp.warn('MCP server is not running') + resolve() + return + } + + try { + this.server.close(() => { + this.server = null + logger.mcp.log('MCP server stopped') + resolve() + }) + } + catch (error) { + logger.mcp.errorWithError('Error stopping MCP server:', error) + this.server = null + resolve() + } + }) + } +} + +// h3 utility function: read body from event +async function readBody(event: any): Promise { + const buffers = [] + for await (const chunk of event.node.req) { + buffers.push(chunk) + } + const data = Buffer.concat(buffers).toString() + return JSON.parse(data) +} diff --git a/services/twitter-services/src/config/index.ts b/services/twitter-services/src/config/index.ts new file mode 100644 index 00000000..c48e4132 --- /dev/null +++ b/services/twitter-services/src/config/index.ts @@ -0,0 +1,113 @@ +import type { Config } from './types' + +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { defu } from 'defu' +import { config as configDotenv } from 'dotenv' + +import { logger } from '../utils/logger' +import { getDefaultConfig } from './types' + +/** + * Load environment variable files + * Load in order of priority + */ +function loadEnvFiles(): void { + // Load environment variable files + const envFiles = [ + '.env.local', + ] + + // Look for .env files from current directory upward + for (const file of envFiles) { + const filePath = path.resolve(process.cwd(), file) + if (fs.existsSync(filePath)) { + const result = configDotenv({ + path: filePath, + override: true, // Allow overriding existing environment variables + }) + + if (result.parsed) { + logger.config.withFields({ + config: result.parsed, + }).log(`Loaded environment variables from ${file}`) + } + } + } +} + +/** + * Configuration manager + * Responsible for loading, validating and providing configuration + */ +export class ConfigManager { + private config: Config + + /** + * Create configuration manager + * @param configPath Path to configuration file + */ + constructor(configPath?: string) { + // First load environment variables + loadEnvFiles() + + // Set default configuration + this.config = getDefaultConfig() + + // Then load from configuration file (if specified) + if (configPath) { + this.loadFromFile(configPath) + } + } + + /** + * Load configuration from file + */ + private loadFromFile(filePath: string): void { + try { + const configFile = fs.readFileSync(filePath, 'utf8') + const fileConfig = JSON.parse(configFile) + + // Use defu to deeply merge configurations + // Values in fileConfig take precedence over this.config + this.config = defu(fileConfig, this.config) + + logger.config.log(`Configuration loaded from ${filePath}`) + } + catch (error) { + logger.config.errorWithError(`Failed to load configuration file: ${(error as Error).message}`, error) + } + } + + /** + * Get complete configuration + */ + getConfig(): Config { + return this.config + } + + /** + * Update configuration + */ + updateConfig(newConfig: Partial): void { + // Use defu to merge new configuration + this.config = defu(newConfig, this.config) + } +} + +// Singleton instance +let configInstance: ConfigManager | null = null + +/** + * Create default configuration manager (singleton) + */ +export function useConfigManager(): ConfigManager { + if (configInstance) { + return configInstance + } + + const configPath = process.env.CONFIG_PATH || path.join(process.cwd(), 'twitter-config.json') + configInstance = new ConfigManager(fs.existsSync(configPath) ? configPath : undefined) + return configInstance +} diff --git a/services/twitter-services/src/config/types.ts b/services/twitter-services/src/config/types.ts new file mode 100644 index 00000000..5c631318 --- /dev/null +++ b/services/twitter-services/src/config/types.ts @@ -0,0 +1,91 @@ +import type { BrowserConfig } from '../types/browser' +import type { SearchOptions, TimelineOptions } from '../types/twitter' + +import process from 'node:process' + +/** + * Complete configuration interface + */ +export interface Config { + // Browser configuration + browser: BrowserConfig & { + apiKey: string // API Key for Stagehand + endpoint?: string // Optional Stagehand service endpoint + } + + // Twitter configuration + twitter: { + defaultOptions?: { + timeline?: TimelineOptions + search?: SearchOptions + } + } + + // Adapter configuration + adapters: { + airi?: { + url?: string + token?: string + enabled: boolean + } + mcp?: { + port?: number + enabled: boolean + } + } + + // System configuration + system: { + logLevel: 'error' | 'warn' | 'info' | 'verbose' | 'debug' + logFormat?: 'json' | 'pretty' + concurrency: number + } +} + +/** + * Default configuration + */ +export function getDefaultConfig(): Config { + // No longer parse cookies from environment variable + // The auth service will load cookies from session file instead + + return { + browser: { + apiKey: process.env.BROWSERBASE_API_KEY || '', // Move apiKey to browser config + headless: process.env.BROWSER_HEADLESS === 'true', + userAgent: process.env.BROWSER_USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + viewport: { + width: Number.parseInt(process.env.BROWSER_VIEWPORT_WIDTH || '1280'), + height: Number.parseInt(process.env.BROWSER_VIEWPORT_HEIGHT || '800'), + }, + timeout: Number.parseInt(process.env.BROWSER_TIMEOUT || '30000'), + requestTimeout: Number.parseInt(process.env.BROWSER_REQUEST_TIMEOUT || '20000'), + requestRetries: Number.parseInt(process.env.BROWSER_REQUEST_RETRIES || '2'), + }, + twitter: { + defaultOptions: { + timeline: { + count: 20, + includeReplies: true, + includeRetweets: true, + }, + }, + }, + adapters: { + airi: { + url: process.env.AIRI_URL || 'http://localhost:3000', + token: process.env.AIRI_TOKEN || '', + enabled: process.env.ENABLE_AIRI === 'true', + }, + mcp: { + port: Number(process.env.MCP_PORT || 8080), + enabled: process.env.ENABLE_MCP === 'true' || true, + }, + }, + system: { + logLevel: 'debug', + logFormat: 'pretty', + concurrency: Number(process.env.CONCURRENCY || 1), + }, + } +} diff --git a/services/twitter-services/src/core/auth-service.ts b/services/twitter-services/src/core/auth-service.ts new file mode 100644 index 00000000..f1c103dc --- /dev/null +++ b/services/twitter-services/src/core/auth-service.ts @@ -0,0 +1,614 @@ +import type { BrowserContext, Cookie, Page } from 'playwright' + +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +import { logger } from '../utils/logger' +import { SELECTORS } from '../utils/selectors' + +/** + * Playwright storage state type definition + */ +interface StorageState { + cookies: Cookie[] + origins: { + origin: string + localStorage: { + name: string + value: string + }[] + }[] + path?: string +} + +/** + * Simple session manager for storing and retrieving browser session data + */ +class SessionManager { + private sessionPath: string + + constructor() { + this.sessionPath = path.join(process.cwd(), 'data', 'twitter-session.json') + } + + /** + * Load storage state from disk + */ + async loadStorageState(): Promise { + try { + // Ensure directory exists + const dir = path.dirname(this.sessionPath) + await fs.mkdir(dir, { recursive: true }) + + // Check if file exists + try { + await fs.access(this.sessionPath) + } + catch { + // File doesn't exist + return null + } + + // Read file + const data = await fs.readFile(this.sessionPath, 'utf-8') + return JSON.parse(data) as StorageState + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to load session data') + return null + } + } + + /** + * Save storage state to disk + */ + async saveStorageState(state: StorageState): Promise { + try { + // Ensure directory exists + const dir = path.dirname(this.sessionPath) + await fs.mkdir(dir, { recursive: true }) + + // Write to file + await fs.writeFile(this.sessionPath, JSON.stringify(state, null, 2)) + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to save session data') + } + } +} + +// Singleton instance +const sessionManager = new SessionManager() + +/** + * Twitter Authentication Service + * Handles login and session management + */ +export class TwitterAuthService { + private page: Page + private context: BrowserContext + private isLoggedIn: boolean = false + + constructor(page: Page, context: BrowserContext) { + this.page = page + this.context = context + } + + /** + * Login to Twitter - simplified method that only tries to use session file + * Users are expected to manually login and save the session + */ + async login(): Promise { + logger.auth.log('Starting Twitter login process') + + try { + // Try to login with existing session first + logger.auth.log('Attempting to load session from file') + const sessionSuccess = await this.checkExistingSession() + + if (sessionSuccess) { + logger.auth.log('Successfully logged in with session file') + return true + } + + // Log session failure but don't attempt automatic login + logger.auth.log('No valid session found, manual login is required') + return false + } + catch (error: unknown) { + logger.auth.withError(error as Error).error('Login process failed') + this.isLoggedIn = false + return false + } + } + + /** + * Verify if login was successful + */ + private async verifyLogin(): Promise { + try { + // Try multiple selectors to determine login status + // First check for timeline which is definitive proof of being logged in + try { + await this.page.waitForSelector(SELECTORS.HOME.TIMELINE, { timeout: 15000 }) + + // Login verification successful - automatically save session + this.isLoggedIn = true + try { + await this.saveCurrentSession() + logger.auth.log('✅ Auto-saved session after successful login verification') + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to auto-save session') + } + + return true + } + catch { + // If timeline selector fails, check for other indicators + } + + // Check for profile button which appears when logged in + try { + const profileSelector = '[data-testid="AppTabBar_Profile_Link"]' + await this.page.waitForSelector(profileSelector, { timeout: 5000 }) + + // Profile link found - automatically save session + this.isLoggedIn = true + try { + await this.saveCurrentSession() + logger.auth.log('✅ Auto-saved session after finding profile link') + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to auto-save session') + } + + return true + } + catch { + // Continue to other checks + } + + // Check for login form to confirm NOT logged in + try { + const loginFormSelector = '[data-testid="loginForm"]' + await this.page.waitForSelector(loginFormSelector, { timeout: 3000 }) + // If login form is visible, we're definitely not logged in + return false + } + catch { + // Login form not found, could still be logged in or on another page + } + + // If we got here, we couldn't definitively confirm login status + // Check current URL for additional clues + const currentUrl = await this.page.evaluate(` + (() => { + return window.location.href; + })() + `) + if (currentUrl.includes('/home')) { + // On home page but couldn't find timeline - might still be loading + return true + } + + // Default to not logged in if we can't confirm + return false + } + catch (error) { + logger.auth.withError(error as Error).error('Error during login verification') + return false + } + } + + /** + * Check current login status + */ + async checkLoginStatus(): Promise { + try { + await this.page.goto('https://x.com/home') + const isLoggedIn = await this.verifyLogin() + + // If already logged in, update state and automatically save session + if (isLoggedIn && !this.isLoggedIn) { + this.isLoggedIn = true + try { + await this.saveCurrentSession() + logger.auth.log('✅ Auto-saved session during status check') + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to auto-save session during status check') + } + } + + return isLoggedIn + } + catch { + return false + } + } + + /** + * Get login status + */ + isAuthenticated(): boolean { + return this.isLoggedIn + } + + /** + * Get current page URL + * @returns Current URL of the Twitter page + */ + async getCurrentUrl(): Promise { + try { + const currentUrl = await this.page.evaluate(` + (() => { + return window.location.href; + })() + `) + return currentUrl + } + catch (error) { + logger.auth.withError(error as Error).error('Error getting current URL') + throw new Error('Failed to get current URL') + } + } + + /** + * Export cookies from the browser context + * @param format - The format of the returned cookies ('object' or 'string') + */ + async exportCookies(format: 'object' | 'string' = 'object'): Promise | string> { + try { + // Get all cookies from browser + const allCookies = await this.context.cookies() + + if (format === 'string') { + // Convert cookie objects to string format + const cookieString = allCookies + .map(cookie => `${cookie.name}=${cookie.value}`) + .join('; ') + + logger.auth.log(`Exported ${allCookies.length} cookies as string`) + return cookieString + } + else { + // Convert to object format + const cookiesObj = allCookies.reduce>((acc, cookie) => { + acc[cookie.name] = cookie.value + return acc + }, {}) + + logger.auth.log(`Exported ${Object.keys(cookiesObj).length} cookies as object`) + return cookiesObj + } + } + catch (error) { + logger.auth.withError(error as Error).error('Failed to export cookies') + if (format === 'string') { + return '' + } + return {} + } + } + + /** + * Login to Twitter using cookies + */ + async loginWithCookies(cookies: Record): Promise { + logger.auth.log(`Attempting to login to Twitter using ${Object.keys(cookies).length} cookies`) + + try { + // Navigate to a Twitter page + await this.page.goto('https://x.com') + + // Convert cookies object to array format required by setCookies + const cookieArray = Object.entries(cookies).map(([name, value]) => ({ + name, + value, + domain: '.x.com', + path: '/', + })) + + // Set cookies using the browser adapter's API that can set HTTP_ONLY cookies + await this.context.addCookies(cookieArray) + + logger.auth.log(`Set ${cookieArray.length} cookies via browser API`) + + // Refresh page to apply cookies + await this.page.goto('https://x.com/home') + + // Verify if login was successful - try multiple times with longer timeout + logger.auth.log('Cookies set, verifying login status...') + + // Try multiple times with increasing timeouts for verification + // Twitter might be slow to respond or need multiple page refreshes + let loginSuccess = false + const verificationAttempts = 3 + + for (let attempt = 1; attempt <= verificationAttempts; attempt++) { + try { + logger.auth.log(`Verification attempt ${attempt}/${verificationAttempts}`) + loginSuccess = await this.verifyLogin() + + if (loginSuccess) { + break + } + else if (attempt < verificationAttempts) { + // If not successful but not last attempt, refresh page and wait + logger.auth.log('Refreshing page and trying again...') + await this.page.goto('https://x.com/home') + await new Promise(resolve => setTimeout(resolve, 3000)) + } + } + catch (error: unknown) { + logger.auth.withError(error as Error).debug(`Verification attempt ${attempt} failed`) + } + } + + if (loginSuccess) { + logger.auth.log('Login with cookies successful') + this.isLoggedIn = true + + // Try to refresh cookies to ensure they're up to date + try { + await this.saveCurrentSession() + logger.auth.log('✅ Session saved to file') + } + catch (error: unknown) { + logger.auth.withError(error as Error).debug('Failed to save session, but login was successful') + } + } + else { + logger.auth.warn('Login with cookies verification failed, cookies may be expired') + } + + return loginSuccess + } + catch (error: unknown) { + logger.auth.withError(error as Error).error('Error during cookie login process') + this.isLoggedIn = false + return false + } + } + + /** + * Attempt to login with an existing session if available + */ + async checkExistingSession(): Promise { + try { + // Get the session data + const sessionData = await sessionManager.loadStorageState() + + if (!sessionData || !sessionData.cookies || sessionData.cookies.length === 0) { + logger.auth.log('No valid session data found') + return false + } + + logger.auth.log(`Found session file with ${sessionData.cookies.length} cookies, attempting login`) + + // Login with the session data + return await this.loginWithSessionData(sessionData) + } + catch (error) { + logger.auth.withError(error as Error).warn('Error checking existing session') + return false + } + } + + /** + * Initiate manual login process with username and password + * @param username Twitter username or email + * @param password Twitter password + */ + async initiateManualLogin(username?: string, password?: string): Promise { + logger.auth.log('Initiating manual login process') + + try { + // Navigate to login page + await this.page.goto('https://x.com/login') + + // Wait for login form to appear and enter credentials + try { + // Wait for username input + await this.page.waitForSelector(SELECTORS.LOGIN.USERNAME_INPUT, { timeout: 10000 }) + + // Use provided credentials if available, otherwise fall back to env vars + const loginUsername = username || process.env.TWITTER_USERNAME + const loginPassword = password || process.env.TWITTER_PASSWORD + + if (!loginUsername || !loginPassword) { + logger.auth.warn('Missing Twitter credentials, manual login cannot proceed') + return false + } + + // Enter username + await this.page.fill(SELECTORS.LOGIN.USERNAME_INPUT, loginUsername) + logger.auth.debug('Username entered') + + // Click next button + await this.page.click(SELECTORS.LOGIN.NEXT_BUTTON) + logger.auth.debug('Next button clicked') + + // Wait for password input + await this.page.waitForSelector(SELECTORS.LOGIN.PASSWORD_INPUT, { timeout: 10000 }) + + // Enter password + await this.page.fill(SELECTORS.LOGIN.PASSWORD_INPUT, loginPassword) + logger.auth.debug('Password entered') + + // Click login button + await this.page.click(SELECTORS.LOGIN.LOGIN_BUTTON) + logger.auth.debug('Login button clicked') + } + catch (error) { + logger.auth.withError(error as Error).error('Error during manual login process') + return false + } + + // Wait for login success at intervals + let attempts = 0 + const maxAttempts = 60 // 10 minutes (10 seconds * 60) + let lastUrl = await this.page.evaluate(` + (() => { + return window.location.href; + })() + `) + + while (attempts < maxAttempts) { + attempts++ + + try { + // Get current URL to detect page changes + const currentUrl = await this.page.evaluate(` + (() => { + return window.location.href; + })() + `) + + // Check if URL has changed significantly - may indicate user interaction + if (currentUrl !== lastUrl && !currentUrl.includes('/flow/login')) { + logger.auth.log(`Detected page change: ${lastUrl} -> ${currentUrl}`) + logger.auth.log('Attempting to navigate to home page and verify login status') + + // URL changed - try navigating to home to verify + await this.page.goto('https://x.com/home') + + // Check if login was successful + const isLoggedIn = await this.verifyLogin() + if (isLoggedIn) { + logger.auth.log('✅ Login successful! Exporting cookies...') + + // Export cookies for future use + try { + const cookies = await this.exportCookies('object') + logger.auth.log(`✅ Successfully exported ${typeof cookies === 'string' ? cookies.length : Object.keys(cookies).length} cookies`) + + // Save the current session to file + await this.saveCurrentSession() + logger.auth.log('✅ Session saved to file') + } + catch (error) { + logger.auth.withError(error as Error).error('Error exporting cookies') + } + + this.isLoggedIn = true + return true + } + + // Update last URL + lastUrl = currentUrl + } + + // Also try direct login verification + const isLoggedIn = await this.verifyLogin() + if (isLoggedIn) { + logger.auth.log('✅ Login successful! Exporting cookies...') + + // Export cookies for future use + try { + const cookies = await this.exportCookies('object') + logger.auth.log(`✅ Successfully exported ${typeof cookies === 'string' ? cookies.length : Object.keys(cookies).length} cookies`) + + // Save the current session to file + await this.saveCurrentSession() + logger.auth.log('✅ Session saved to file') + } + catch (error) { + logger.auth.withError(error as Error).error('Error exporting cookies') + } + + this.isLoggedIn = true + return true + } + } + catch (error) { + // Ignore errors during verification, continue polling + logger.auth.debug(`Error during verification: ${(error as Error).message}`) + } + + // Wait 10 seconds before checking again + await new Promise(resolve => setTimeout(resolve, 10000)) + + // Only log every 6 attempts (1 minute) to reduce noise + if (attempts % 6 === 0) { + logger.auth.log(`Still waiting for login... (${Math.floor(attempts / 6)} minutes elapsed)`) + } + } + + logger.auth.warn('⚠️ Manual login timeout exceeded') + return false + } + catch (error) { + logger.auth.withError(error as Error).error('Error during manual login process') + return false + } + } + + /** + * Save the current session to a file + */ + async saveCurrentSession(): Promise { + try { + // Get the storage state directly from context + const storageState = await this.context.storageState() + + // Save the session using the session manager + await sessionManager.saveStorageState(storageState) + + logger.auth.log('✅ Session saved to file using browserContext.storageState()') + } + catch (error) { + logger.auth.withError(error as Error).warn('Failed to save session') + } + } + + /** + * Login with stored session data + */ + private async loginWithSessionData(sessionData: StorageState): Promise { + try { + // Extract cookies from session data + const { cookies } = sessionData + + // Create array of cookie objects for the browser + const cookieArray = cookies.map(cookie => ({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path, + })) + + // Set cookies using the browser adapter's API + await this.context.addCookies(cookieArray) + logger.auth.log(`Set ${cookieArray.length} cookies from session file`) + + // Set localStorage if available + if (sessionData.origins && sessionData.origins.length > 0) { + await this.context.storageState(sessionData) + logger.auth.log(`Set localStorage for ${sessionData.origins.length} origins`) + } + + // Navigate to home to verify login + await this.page.goto('https://x.com/home') + + // Verify if login was successful + const loginSuccess = await this.verifyLogin() + + if (loginSuccess) { + this.isLoggedIn = true + logger.auth.log('✅ Successfully logged in with session data') + } + else { + logger.auth.warn('⚠️ Session data login failed verification') + } + + return loginSuccess + } + catch (error) { + logger.auth.withError(error as Error).error('Failed to login with session data') + return false + } + } +} diff --git a/services/twitter-services/src/core/timeline-service.ts b/services/twitter-services/src/core/timeline-service.ts new file mode 100644 index 00000000..5b4c6a59 --- /dev/null +++ b/services/twitter-services/src/core/timeline-service.ts @@ -0,0 +1,118 @@ +import type { Page } from 'playwright' +import type { TimelineOptions, Tweet } from '../types/twitter' + +import { TweetParser } from '../parsers/tweet-parser' +import { logger } from '../utils/logger' +import { SELECTORS } from '../utils/selectors' + +/** + * Twitter Timeline Service + * Handles fetching and parsing timeline content + */ +export class TwitterTimelineService { + private page: Page + + constructor(page: Page) { + this.page = page + } + + /** + * Fetches the Twitter timeline + * @param options Configuration options for timeline fetching + * @returns Promise resolving to an array of tweets + */ + async getTimeline(options: TimelineOptions = {}): Promise { + try { + logger.timeline.withFields({ options }).log('Fetching timeline') + + // Navigate to home page + await this.page.goto('https://x.com/home') + + // Wait for timeline to load + await this.page.waitForSelector(SELECTORS.TIMELINE.TWEET, { timeout: 10000 }) + + // Optional: scroll to load more tweets if needed + if (options.count && options.count > 5) { + await this.scrollToLoadMoreTweets(Math.min(options.count, 20)) + } + + // Parse all tweets directly from the DOM using Playwright + const tweets = await TweetParser.parseTimelineTweets(this.page) + + logger.timeline.log(`Found ${tweets.length} tweets in timeline`) + + // Apply filters + let filteredTweets = tweets + + if (options.includeReplies === false) { + filteredTweets = filteredTweets.filter(tweet => !tweet.text.startsWith('@')) + } + + if (options.includeRetweets === false) { + filteredTweets = filteredTweets.filter(tweet => !tweet.text.startsWith('RT @')) + } + + // Apply count limit if specified + if (options.count) { + filteredTweets = filteredTweets.slice(0, options.count) + } + + return filteredTweets + } + catch (error) { + logger.timeline.error('Failed to get timeline:', (error as Error).message) + return [] + } + } + + /** + * Scrolls down the timeline to load more tweets + * @param targetCount Approximate number of tweets to load + */ + private async scrollToLoadMoreTweets(targetCount: number): Promise { + try { + // Initial tweet count + let previousTweetCount = 0 + let currentTweetCount = await this.countVisibleTweets() + let scrollAttempts = 0 + const maxScrollAttempts = 10 + + logger.timeline.log(`Initial tweet count: ${currentTweetCount}, target: ${targetCount}`) + + // Scroll until we have enough tweets or reach maximum scroll attempts + while (currentTweetCount < targetCount && scrollAttempts < maxScrollAttempts) { + // Scroll down using Playwright's mouse wheel simulation + await this.page.mouse.wheel(0, 800) + + // Wait for new content to load + await this.page.waitForTimeout(1000) + + // Check if we have new tweets + previousTweetCount = currentTweetCount + currentTweetCount = await this.countVisibleTweets() + + // If no new tweets were loaded, we might have reached the end + if (currentTweetCount === previousTweetCount) { + scrollAttempts++ + } + else { + scrollAttempts = 0 // Reset counter if we're still loading tweets + } + + logger.timeline.debug(`Scrolled for more tweets: ${currentTweetCount}/${targetCount}`) + } + } + catch (error) { + logger.timeline.error('Error while scrolling for more tweets:', (error as Error).message) + } + } + + /** + * Counts the number of visible tweets on the page + * @returns Promise resolving to the count of visible tweets + */ + private async countVisibleTweets(): Promise { + const tweetElements = await this.page.$$(SELECTORS.TIMELINE.TWEET) + return tweetElements.length + } +} diff --git a/services/twitter-services/src/core/twitter-service.ts b/services/twitter-services/src/core/twitter-service.ts new file mode 100644 index 00000000..d7f91c0c --- /dev/null +++ b/services/twitter-services/src/core/twitter-service.ts @@ -0,0 +1,222 @@ +import type { PostOptions, SearchOptions, TimelineOptions, Tweet, TweetDetail, UserProfile } from '../types/twitter' +import type { TwitterAuthService } from './auth-service' +import type { TwitterTimelineService } from './timeline-service' + +import { logger } from '../utils/logger' + +export class TwitterService { + private authService: TwitterAuthService + private timelineService: TwitterTimelineService + private sessionMonitorInterval: NodeJS.Timeout | null = null + + constructor(authService: TwitterAuthService, timelineService: TwitterTimelineService) { + this.authService = authService + this.timelineService = timelineService + } + + /** + * Login to Twitter + * Attempts to restore session from saved cookies first + * If that fails, will need manual login in the browser + */ + async login(): Promise { + try { + // Try to restore session from cookies + const success = await this.authService.login() + + if (success) { + logger.main.log('Successfully restored Twitter session from cookies') + return true + } + + logger.main.log('No saved session found, waiting for manual login') + + // Set up session monitoring to detect when user has manually logged in + this.startSessionMonitor() + + return false + } + catch (error) { + logger.main.error('Error during login:', (error as Error).message) + return false + } + } + + /** + * Get timeline + */ + async getTimeline(options?: TimelineOptions): Promise { + this.ensureAuthenticated() + return this.timelineService.getTimeline(options) + } + + /** + * Get tweet details + */ + async getTweetDetails(tweetId: string): Promise { + this.ensureAuthenticated() + // This is a stub implementation + return { + id: tweetId, + text: 'Tweet details feature not yet implemented', + author: { + username: 'twitter', + displayName: 'Twitter', + }, + timestamp: new Date().toISOString(), + } + } + + /** + * Search tweets + */ + async searchTweets(_query: string, _options?: SearchOptions): Promise { + throw new Error('Search feature not yet implemented') + } + + /** + * Get user profile + */ + async getUserProfile(_username: string): Promise { + throw new Error('Get user profile feature not yet implemented') + } + + /** + * Follow user (not implemented in MVP) + */ + async followUser(_username: string): Promise { + this.ensureAuthenticated() + return false + } + + /** + * Like tweet + */ + async likeTweet(_tweetId: string): Promise { + throw new Error('Like feature not yet implemented') + } + + /** + * Retweet + */ + async retweet(_tweetId: string): Promise { + throw new Error('Retweet feature not yet implemented') + } + + /** + * Post a tweet + */ + async postTweet(_content: string, _options?: PostOptions): Promise { + throw new Error('Post tweet feature not yet implemented') + } + + /** + * Manually trigger a session save + * Typically this is handled by the session monitor + */ + async saveSession(): Promise { + try { + if (!this.authService.isAuthenticated()) { + logger.main.warn('Cannot save session when not authenticated') + return false + } + + await this.authService.saveCurrentSession() + logger.main.log('Successfully saved Twitter session') + return true + } + catch (error) { + logger.main.error('Error saving session:', (error as Error).message) + return false + } + } + + /** + * Ensure the user is authenticated before performing operations + * @private + */ + private ensureAuthenticated(): void { + if (!this.authService.isAuthenticated()) { + throw new Error('You must be logged in to perform this action. Please call login() first.') + } + } + + /** + * Export the current session cookies + * @param format The format to export cookies in + */ + async exportCookies(format: 'object' | 'string' = 'object'): Promise | string> { + this.ensureAuthenticated() + return this.authService.exportCookies(format) + } + + /** + * Start monitoring for session changes + * This will periodically check if the user is logged in + * and save the session if they are + * @param interval Time in ms between checks + */ + startSessionMonitor(interval: number = 30000): void { + // Clear any existing monitor + if (this.sessionMonitorInterval) { + clearInterval(this.sessionMonitorInterval) + } + + logger.main.log(`Starting Twitter session monitor with ${interval}ms interval`) + + this.sessionMonitorInterval = setInterval(async () => { + try { + await this.checkAndSaveSession() + } + catch (error) { + logger.main.error('Error in session monitor:', (error as Error).message) + } + }, interval) + } + + /** + * Get the current page URL + * Useful for debugging and checking the current state + */ + async getCurrentUrl(): Promise { + try { + return await this.authService.getCurrentUrl() + } + catch (error) { + logger.main.error('Error getting current URL:', (error as Error).message) + return 'unknown' + } + } + + /** + * Stop the session monitor + */ + stopSessionMonitor(): void { + if (this.sessionMonitorInterval) { + clearInterval(this.sessionMonitorInterval) + this.sessionMonitorInterval = null + logger.main.log('Stopped Twitter session monitor') + } + } + + /** + * Check and save the session if logged in + * @private + */ + private async checkAndSaveSession(): Promise { + try { + const isLoggedIn = await this.authService.checkLoginStatus() + + if (isLoggedIn && !this.authService.isAuthenticated()) { + logger.main.log('User has logged in manually, saving session') + await this.saveSession() + } + else if (!isLoggedIn && this.authService.isAuthenticated()) { + logger.main.warn('User appears to be logged out, updating state') + } + } + catch (error) { + logger.main.error('Error checking session status:', (error as Error).message) + } + } +} diff --git a/services/twitter-services/src/main.ts b/services/twitter-services/src/main.ts new file mode 100644 index 00000000..1f338d75 --- /dev/null +++ b/services/twitter-services/src/main.ts @@ -0,0 +1,188 @@ +import type { Browser, BrowserContext, Page } from 'playwright' +import type { AiriAdapter } from './adapters/airi-adapter' +import type { MCPAdapter } from './adapters/mcp-adapter' +import type { Config } from './config/types' + +import process from 'node:process' +import { chromium } from 'playwright' + +import { useConfigManager } from './config' +import { TwitterAuthService } from './core/auth-service' +import { TwitterTimelineService } from './core/timeline-service' +import { TwitterService } from './core/twitter-service' +import { initLogger, logger } from './utils/logger' + +/** + * Initialize browser and create page + */ +async function initBrowser(config: Config): Promise<{ browser: Browser, context: BrowserContext, page: Page }> { + const browser = await chromium.launch({ + headless: config.browser.headless, + }) + + const context = await browser.newContext({ + userAgent: config.browser.userAgent, + viewport: config.browser.viewport, + bypassCSP: true, + }) + + context.setDefaultTimeout(config.browser.timeout || 30000) + const page = await context.newPage() + + // Navigate to Twitter login page by default + await page.goto('https://x.com/login') + + logger.main.log('Browser initialized') + return { browser, context, page } +} + +/** + * Initialize Twitter service and login + */ +async function initTwitterService(page: Page, context: BrowserContext, _config: Config): Promise { + const authService = new TwitterAuthService(page, context) + const timelineService = new TwitterTimelineService(page) + const twitterService = new TwitterService(authService, timelineService) + + // Check if we have a saved session + try { + const sessionSuccess = await authService.checkExistingSession() + if (sessionSuccess) { + logger.main.log('Successfully loaded existing Twitter session') + } + else { + // Instead of automatic login, navigate to login page + logger.main.log('No valid session found, navigating to login page for manual login') + await page.goto('https://x.com/login') + } + } + catch (error) { + logger.main.withError(error as Error).warn('Error checking session, navigating to login page') + await page.goto('https://x.com/login') + } + + // Start session monitoring to automatically save session when user logs in + twitterService.startSessionMonitor() + logger.main.log('Started automatic session monitoring') + + return twitterService +} + +/** + * Initialize adapters + */ +async function initAdapters(twitterService: TwitterService, config: Config): Promise<{ airi?: AiriAdapter, mcp?: MCPAdapter }> { + const adapters: { airi?: AiriAdapter, mcp?: MCPAdapter } = {} + + // if (config.adapters.airi?.enabled) { + // logger.main.log('Starting Airi adapter...') + // const { AiriAdapter } = await import('./adapters/airi-adapter') + + // adapters.airi = new AiriAdapter(twitterService, { + // url: config.adapters.airi.url, + // token: config.adapters.airi.token, + // credentials: {}, + // }) + + // await adapters.airi.start() + // logger.main.log('Airi adapter started') + // } + + if (config.adapters.mcp?.enabled) { + logger.main.log('Starting MCP adapter...') + const { MCPAdapter } = await import('./adapters/mcp-adapter') + + adapters.mcp = new MCPAdapter( + twitterService, + config.adapters.mcp.port, + ) + + await adapters.mcp.start() + logger.main.log('MCP adapter started') + } + + return adapters +} + +/** + * Clean up resources + */ +async function cleanup( + adapters: { airi?: AiriAdapter, mcp?: MCPAdapter }, + context?: BrowserContext, + browser?: Browser, +) { + logger.main.log('Stopping Twitter service...') + + if (adapters.mcp) { + await adapters.mcp.stop() + logger.main.log('MCP adapter stopped') + } + + if (context) { + await context.close() + } + + if (browser) { + await browser.close() + logger.main.log('Browser closed') + } + + logger.main.log('Twitter service stopped') +} + +/** + * Set up process shutdown hooks + */ +function setupShutdownHooks( + adapters: { airi?: AiriAdapter, mcp?: MCPAdapter }, + context?: BrowserContext, + browser?: Browser, +) { + const handleShutdown = async (signal: string) => { + logger.main.log(`Received ${signal} signal...`) + await cleanup(adapters, context, browser) + process.exit(0) + } + + process.on('SIGINT', () => handleShutdown('exit')) + process.on('SIGTERM', () => handleShutdown('termination')) + + process.on('uncaughtException', async (error) => { + logger.main.withError(error).error('Uncaught exception') + await cleanup(adapters, context, browser) + process.exit(1) + }) +} + +// Start application +async function bootstrap() { + // Initialize logging system + initLogger() + + try { + const config = useConfigManager().getConfig() + logger.main.log('Starting Twitter service...') + + // Initialize core components + const { browser, context, page } = await initBrowser(config) + const twitterService = await initTwitterService(page, context, config) + const adapters = await initAdapters(twitterService, config) + + // Set up shutdown hooks + setupShutdownHooks(adapters, context, browser) + + logger.main.log('Twitter service successfully started!') + } + catch (error) { + logger.main.withError(error).error('Startup failed') + process.exit(1) + } + + // Handle unhandled rejections + process.on('unhandledRejection', (reason) => { + logger.main.withError(reason).error('Unhandled Promise rejection:') + }) +} + +bootstrap() diff --git a/services/twitter-services/src/parsers/profile-parser.ts b/services/twitter-services/src/parsers/profile-parser.ts new file mode 100644 index 00000000..d29e0577 --- /dev/null +++ b/services/twitter-services/src/parsers/profile-parser.ts @@ -0,0 +1,293 @@ +import type { Page } from 'playwright' +import type { UserLink, UserProfile, UserStats } from '../types/twitter' + +import { logger } from '../utils/logger' +import { SELECTORS } from '../utils/selectors' + +/** + * Profile Parser + * Extracts user profile information directly from the page DOM using Playwright + */ +export class ProfileParser { + /** + * Parse user profile from a Twitter profile page + * @param page Playwright page instance + * @returns Promise resolving to UserProfile object + */ + static async parseUserProfile(page: Page): Promise { + try { + // Extract basic profile info + const displayNameElement = await page.$(SELECTORS.PROFILE.DISPLAY_NAME) + const displayName = await displayNameElement?.textContent() || 'Unknown User' + + // Get username from URL or profile elements + let username = '' + const url = page.url() + const urlUsername = this.extractUsernameFromUrl(url) + + if (urlUsername) { + username = urlUsername + } + else { + // Try to find username in the DOM + const usernameElement = await page.$('[data-testid="UserName"] span:has-text("@")') + const usernameText = await usernameElement?.textContent() + username = usernameText?.replace('@', '') || 'unknown' + } + + // Get bio + const bioElement = await page.$(SELECTORS.PROFILE.BIO) + const bio = await bioElement?.textContent() + + // Get profile images + const avatarUrl = await this.extractAvatarUrl(page) + const bannerUrl = await this.extractBannerUrl(page) + + // Get statistics + const stats = await this.extractUserStats(page) + + // Get join date + const joinDate = await this.extractJoinDate(page) + + // Get user links + // const _links = await this.extractUserLinks(page) + + const profile: UserProfile = { + username, + displayName, + } + + // Add optional fields if they exist + if (bio) + profile.bio = bio + if (avatarUrl) + profile.avatarUrl = avatarUrl + if (bannerUrl) + profile.bannerUrl = bannerUrl + if (stats.followers) + profile.followersCount = stats.followers + if (stats.following) + profile.followingCount = stats.following + if (stats.tweets) + profile.tweetCount = stats.tweets + if (joinDate) + profile.joinDate = joinDate + + // Check for verification badge + const isVerified = await page.$('[data-testid="icon-verified"]') !== null + if (isVerified) + profile.isVerified = true + + return profile + } + catch (error) { + logger.parser.error('Error parsing user profile:', (error as Error).message) + + // Return minimal profile to avoid breaking + return { + username: 'unknown', + displayName: 'Unknown User', + } + } + } + + /** + * Extract username from Twitter profile URL + * @param url Twitter profile URL + * @returns Username or null if not found + */ + private static extractUsernameFromUrl(url: string): string | null { + try { + const match = url.match(/twitter\.com\/([^/]+)/) + if (match && match[1] && !['home', 'explore', 'notifications', 'messages'].includes(match[1])) { + return match[1] + } + return null + } + catch { + return null + } + } + + /** + * Extract user statistics (followers, following, tweets) + * @param page Playwright page instance + * @returns Promise resolving to UserStats object + */ + private static async extractUserStats(page: Page): Promise { + const stats: UserStats = { + followers: 0, + following: 0, + tweets: 0, + } + + try { + // Get stats container + const statsContainer = await page.$(SELECTORS.PROFILE.STATS) + if (!statsContainer) + return stats + + // Get all stat items + const statItems = await statsContainer.$$('a') + + for (const statItem of statItems) { + const text = await statItem.textContent() || '' + + if (text.includes('Following')) { + const countText = text.replace(/Following.*/, '').trim() + stats.following = this.parseStatNumber(countText) + } + else if (text.includes('Followers')) { + const countText = text.replace(/Followers.*/, '').trim() + stats.followers = this.parseStatNumber(countText) + } + else if (text.includes('posts') || text.includes('Posts')) { + const countText = text.replace(/posts|Posts.*/, '').trim() + stats.tweets = this.parseStatNumber(countText) + } + } + + return stats + } + catch (error) { + logger.parser.error('Error extracting user stats:', (error as Error).message) + return stats + } + } + + /** + * Extract profile avatar URL + * @param page Playwright page instance + * @returns Promise resolving to avatar URL or undefined + */ + private static async extractAvatarUrl(page: Page): Promise { + try { + const avatarElement = await page.$('img[src*="profile_images"]') + const src = await avatarElement?.getAttribute('src') + return src || undefined + } + catch (error) { + logger.parser.error('Error extracting avatar URL:', (error as Error).message) + return undefined + } + } + + /** + * Extract profile banner URL + * @param page Playwright page instance + * @returns Promise resolving to banner URL or undefined + */ + private static async extractBannerUrl(page: Page): Promise { + try { + const bannerElement = await page.$('img[src*="profile_banners"]') + const src = await bannerElement?.getAttribute('src') + return src || undefined + } + catch (error) { + logger.parser.error('Error extracting banner URL:', (error as Error).message) + return undefined + } + } + + /** + * Extract join date from profile + * @param page Playwright page instance + * @returns Promise resolving to join date string or undefined + */ + private static async extractJoinDate(page: Page): Promise { + try { + // Try to find join date text that usually appears as "Joined Month Year" + const joinedText = await page.$('span:has-text("Joined")') + if (!joinedText) + return undefined + + const fullText = await joinedText.textContent() + if (fullText && fullText.includes('Joined')) { + // Extract just the date part + const datePart = fullText.replace('Joined', '').trim() + return datePart || undefined + } + + return undefined + } + catch (error) { + logger.parser.error('Error extracting join date:', (error as Error).message) + return undefined + } + } + + /** + * Extract user links (website, location) + * @param page Playwright page instance + * @returns Promise resolving to array of user links + */ + private static async extractUserLinks(page: Page): Promise { + const links: UserLink[] = [] + + try { + // Find all link elements in profile + const linkElements = await page.$$('a[href^="https"]:not([href*="twitter.com"])') + + for (const linkElement of linkElements) { + // Extract href and title + const href = await linkElement.getAttribute('href') + const title = await linkElement.textContent() + + if (href && title) { + links.push({ + type: 'url', + url: href, + title, + }) + } + } + + // Try to find location + const locationElement = await page.$('span:has-text("Location")') + if (locationElement) { + const locationText = await locationElement.textContent() + if (locationText) { + links.push({ + type: 'location', + url: '', + title: locationText.replace('Location', '').trim(), + }) + } + } + + return links + } + catch (error) { + logger.parser.error('Error extracting user links:', (error as Error).message) + return links + } + } + + /** + * Parse stat number with K, M suffixes + * @param text Number text (e.g., "10.5K") + * @returns Parsed number + */ + private static parseStatNumber(text: string): number { + try { + text = text.trim() + + if (!text) + return 0 + + if (text.includes('K')) { + return Math.round(Number.parseFloat(text.replace('K', '')) * 1000) + } + else if (text.includes('M')) { + return Math.round(Number.parseFloat(text.replace('M', '')) * 1000000) + } + + // Handle other formats like 1,234 + const normalized = text.replace(/,/g, '') + return Number.parseInt(normalized, 10) || 0 + } + catch { + return 0 + } + } +} diff --git a/services/twitter-services/src/parsers/tweet-parser.ts b/services/twitter-services/src/parsers/tweet-parser.ts new file mode 100644 index 00000000..1fb7d0b8 --- /dev/null +++ b/services/twitter-services/src/parsers/tweet-parser.ts @@ -0,0 +1,260 @@ +import type { ElementHandle, Page } from 'playwright' +import type { Tweet } from '../types/twitter' + +import { logger } from '../utils/logger' +import { SELECTORS } from '../utils/selectors' + +/** + * Tweet Parser + * Extracts tweet information directly from the page DOM using Playwright + */ +export class TweetParser { + /** + * Parse timeline tweets directly from the page + * @param page Playwright page instance + * @returns Promise resolving to Tweet array + */ + static async parseTimelineTweets(page: Page): Promise { + try { + const tweetElements = await page.$$(SELECTORS.TIMELINE.TWEET) + logger.parser.log(`Found ${tweetElements.length} tweet elements`) + + const tweets: Tweet[] = [] + + for (const tweetElement of tweetElements) { + const tweet = await this.extractTweetData(page, tweetElement) + if (tweet) { + tweets.push(tweet) + } + } + + return tweets + } + catch (error) { + logger.parser.error('Error parsing timeline tweets:', (error as Error).message) + return [] + } + } + + /** + * Extract tweet data from tweet element + * @param page Playwright page instance + * @param tweetElement Tweet element handle + * @returns Promise resolving to Tweet object + */ + static async extractTweetData(page: Page, tweetElement: ElementHandle): Promise { + try { + // Extract tweet ID + const id = await this.extractTweetId(tweetElement) + + // Extract tweet text + const textElement = await tweetElement.$(SELECTORS.TIMELINE.TWEET_TEXT) + const text = textElement ? await textElement.textContent() : '' + + // Extract author info + const author = await this.extractAuthorInfo(tweetElement) + + // Extract timestamp + const timeElement = await tweetElement.$('time') + const timestamp = timeElement ? await timeElement.getAttribute('datetime') : new Date().toISOString() + + // Extract engagement stats + const stats = await this.extractTweetStats(tweetElement) + + // Extract media URLs + const mediaUrls = await this.extractMediaUrls(tweetElement) + + const tweet: Tweet = { + id, + text: text || '', + author, + timestamp: timestamp || new Date().toISOString(), + ...stats, + } + + if (mediaUrls.length > 0) { + tweet.mediaUrls = mediaUrls + } + + return tweet + } + catch (error) { + logger.parser.error('Error extracting tweet data:', (error as Error).message) + return null + } + } + + /** + * Extract tweet ID from tweet element + * @param tweetElement Tweet element handle + * @returns Promise resolving to tweet ID + */ + private static async extractTweetId(tweetElement: ElementHandle): Promise { + try { + // Try to get ID from status link + const statusLink = await tweetElement.$('a[href*="/status/"]') + if (statusLink) { + const href = await statusLink.getAttribute('href') + if (href) { + const match = href.match(/\/status\/(\d+)/) + if (match && match[1]) { + return match[1] + } + } + } + + // Fallback to a random ID + return `tweet-${Date.now()}-${Math.floor(Math.random() * 1000)}` + } + catch (error) { + logger.parser.error('Error extracting tweet ID:', (error as Error).message) + return `tweet-${Date.now()}` + } + } + + /** + * Extract author info from tweet element + * @param tweetElement Tweet element handle + * @returns Promise resolving to author object + */ + private static async extractAuthorInfo(tweetElement: ElementHandle): Promise { + try { + // Find author element + const authorElement = await tweetElement.$('[data-testid="User-Name"]') + if (!authorElement) { + return { + username: 'unknown', + displayName: 'Unknown User', + } + } + + // Get display name + const displayNameElement = await authorElement.$('span:first-child') + const displayName = displayNameElement ? await displayNameElement.textContent() || 'Unknown User' : 'Unknown User' + + // Get username + const usernameElement = await authorElement.$('span a[href^="/"]') + let username = usernameElement ? await usernameElement.textContent() : 'unknown' + username = username?.replace('@', '') || 'unknown' + + // Get avatar URL + const avatarElement = await tweetElement.$('img[src*="/profile_images/"]') + const avatarUrl = avatarElement ? await avatarElement.getAttribute('src') : undefined + + return { + username, + displayName, + ...(avatarUrl && { avatarUrl }), + } + } + catch (error) { + logger.parser.error('Error extracting author info:', (error as Error).message) + return { + username: 'unknown', + displayName: 'Unknown User', + } + } + } + + /** + * Extract tweet stats (likes, retweets, replies) + * @param tweetElement Tweet element handle + * @returns Promise resolving to stats object + */ + private static async extractTweetStats(tweetElement: ElementHandle): Promise<{ + likeCount?: number + retweetCount?: number + replyCount?: number + }> { + const stats: { + likeCount?: number + retweetCount?: number + replyCount?: number + } = {} + + try { + // Extract like count + const likeElement = await tweetElement.$(SELECTORS.TIMELINE.LIKE_BUTTON) + if (likeElement) { + const likeCountElement = await likeElement.$('span span') + const likeCountText = likeCountElement ? await likeCountElement.textContent() : null + stats.likeCount = this.parseCount(likeCountText) + } + + // Extract retweet count + const retweetElement = await tweetElement.$(SELECTORS.TIMELINE.RETWEET_BUTTON) + if (retweetElement) { + const retweetCountElement = await retweetElement.$('span span') + const retweetCountText = retweetCountElement ? await retweetCountElement.textContent() : null + stats.retweetCount = this.parseCount(retweetCountText) + } + + // Extract reply count + const replyElement = await tweetElement.$(SELECTORS.TIMELINE.REPLY_BUTTON) + if (replyElement) { + const replyCountElement = await replyElement.$('span span') + const replyCountText = replyCountElement ? await replyCountElement.textContent() : null + stats.replyCount = this.parseCount(replyCountText) + } + + return stats + } + catch (error) { + logger.parser.error('Error extracting tweet stats:', (error as Error).message) + return stats + } + } + + /** + * Extract media URLs from tweet element + * @param tweetElement Tweet element handle + * @returns Promise resolving to array of media URLs + */ + private static async extractMediaUrls(tweetElement: ElementHandle): Promise { + try { + const mediaElements = await tweetElement.$$('img[src*="pbs.twimg.com/media/"]') + + const mediaUrls: string[] = [] + for (const mediaElement of mediaElements) { + const src = await mediaElement.getAttribute('src') + if (src) { + mediaUrls.push(src) + } + } + + return mediaUrls + } + catch (error) { + logger.parser.error('Error extracting media URLs:', (error as Error).message) + return [] + } + } + + /** + * Parse count text (handles K, M suffixes) + * @param countText Count text from tweet + * @returns Parsed number or undefined + */ + private static parseCount(countText: string | null): number | undefined { + if (!countText) + return undefined + + try { + countText = countText.trim() + if (!countText) + return undefined + + if (countText.includes('K')) { + return Math.round(Number.parseFloat(countText.replace('K', '')) * 1000) + } + else if (countText.includes('M')) { + return Math.round(Number.parseFloat(countText.replace('M', '')) * 1000000) + } + + return Number.parseInt(countText, 10) || undefined + } + catch { + return undefined + } + } +} diff --git a/services/twitter-services/src/types/twitter.ts b/services/twitter-services/src/types/twitter.ts new file mode 100644 index 00000000..e446e383 --- /dev/null +++ b/services/twitter-services/src/types/twitter.ts @@ -0,0 +1,85 @@ +/** + * Tweet Interface + */ +export interface Tweet { + id: string + text: string + author: { + username: string + displayName: string + avatarUrl?: string + } + timestamp: string + likeCount?: number + retweetCount?: number + replyCount?: number + mediaUrls?: string[] +} + +/** + * Tweet Detail + */ +export interface TweetDetail extends Tweet { + replies?: Tweet[] + quotedTweet?: Tweet +} + +/** + * User Profile + */ +export interface UserProfile { + username: string + displayName: string + bio?: string + avatarUrl?: string + bannerUrl?: string + followersCount?: number + followingCount?: number + tweetCount?: number + isVerified?: boolean + joinDate?: string +} + +/** + * Timeline Options + */ +export interface TimelineOptions { + count?: number + includeReplies?: boolean + includeRetweets?: boolean + limit?: number +} + +/** + * Search Options + */ +export interface SearchOptions { + count?: number + filter?: 'latest' | 'photos' | 'videos' | 'top' +} + +/** + * Post Options + */ +export interface PostOptions { + media?: string[] + inReplyTo?: string +} + +/** + * User Stats + */ +export interface UserStats { + tweets: number + following: number + followers: number +} + +/** + * User Link + */ +export interface UserLink { + type: string + url: string + title: string +} diff --git a/services/twitter-services/src/utils/error.ts b/services/twitter-services/src/utils/error.ts new file mode 100644 index 00000000..15835674 --- /dev/null +++ b/services/twitter-services/src/utils/error.ts @@ -0,0 +1,74 @@ +/** + * Safely extract error message from any error type + * Handles Error objects, strings, objects, and other types + * + * @param error - Any error object + * @param fallbackMessage - Fallback message when unable to extract a message + * @returns Formatted error message + */ +export function errorToMessage(error: unknown, fallbackMessage = 'Unknown error'): string { + if (error === null || error === undefined) { + return fallbackMessage + } + + // Handle standard Error objects + if (error instanceof Error) { + return error.message + } + + // Handle string errors + if (typeof error === 'string') { + return error + } + + // Handle objects with message property + if (typeof error === 'object') { + // Check if it has a message property + if ('message' in error && typeof (error as any).message === 'string') { + return (error as any).message + } + + // Try to convert object to string + try { + return JSON.stringify(error) + } + catch { + // If serialization fails, return object's string representation + return String(error) + } + } + + // For other cases, try to force convert to string + return String(error) +} + +/** + * Create an error with detailed context information + * + * @param message - Error message + * @param originalError - Original error object (optional) + * @param context - Additional context information (optional) + * @returns Enhanced error object + */ +export function createError( + message: string, + originalError?: unknown, + context?: Record, +): Error { + let errorMessage = message + + // Add original error information + if (originalError) { + errorMessage += `: ${errorToMessage(originalError)}` + } + + // Create new error object + const error = new Error(errorMessage) + + // Add context information + if (context) { + Object.assign(error, { context }) + } + + return error +} diff --git a/services/twitter-services/src/utils/logger.ts b/services/twitter-services/src/utils/logger.ts new file mode 100644 index 00000000..987ef56a --- /dev/null +++ b/services/twitter-services/src/utils/logger.ts @@ -0,0 +1,72 @@ +import path from 'node:path' +import { createLogg, Format, LogLevel, setGlobalFormat, setGlobalLogLevel } from '@guiiai/logg' + +import { useConfigManager } from '../config' + +// Track initialization status +let isInitialized = false + +// Initialize global logging configuration +export function initLogger(): void { + if (isInitialized) { + return // Prevent multiple initializations + } + + // Set global log level + setGlobalLogLevel(LogLevel.Debug) + setGlobalFormat(Format.Pretty) + + const config = useConfigManager().getConfig() + + const logLevelMap: Record = { + error: LogLevel.Error, + warn: LogLevel.Warning, + info: LogLevel.Log, + verbose: LogLevel.Verbose, + debug: LogLevel.Debug, + } + + setGlobalLogLevel(logLevelMap[config.system?.logLevel] || LogLevel.Debug) + + // Set format based on configuration + if (config.system?.logFormat === 'pretty') { + setGlobalFormat(Format.Pretty) + } + else { + setGlobalFormat(Format.JSON) + } + + isInitialized = true +} + +/** + * Get logger instance with directory name and filename + * @returns logger instance configured with "directoryName/filename" + */ +export function useLogger(name?: string): ReturnType { + if (name) + return createLogg(name).useGlobalConfig() + + const stack = new Error('logger').stack + const caller = stack?.split('\n')[2] + + // Extract directory, filename and line number from stack trace + const match = caller?.match(/(?:([^/]+)\/)?([^/\s]+?)(?:\.[jt]s)?:(\d+)(?::\d+)?\)?$/) + const dirName = match?.[1] || path.basename(path.dirname(__filename)) + const fileName = match?.[2] || path.basename(__filename, '.ts') + const lineNumber = match?.[3] || '?' + + return createLogg(`${dirName}/${fileName}:${lineNumber}`).useGlobalConfig() +} + +// Create pre-configured loggers for various services +export const logger = { + auth: useLogger('auth-service'), + timeline: useLogger('timeline-service'), + browser: useLogger('browser-adapter'), + airi: useLogger('airi-adapter'), + mcp: useLogger('mcp-adapter'), + parser: useLogger('parser'), + main: useLogger('twitter-service'), + config: useLogger('config'), +} diff --git a/services/twitter-services/src/utils/rate-limiter.ts b/services/twitter-services/src/utils/rate-limiter.ts new file mode 100644 index 00000000..047d2b42 --- /dev/null +++ b/services/twitter-services/src/utils/rate-limiter.ts @@ -0,0 +1,67 @@ +/** + * Request rate limiter + * Controls request frequency to Twitter to avoid triggering limits + */ +export class RateLimiter { + private requestHistory: number[] = [] + private maxRequests: number + private timeWindow: number + + /** + * Create rate limiter + * @param maxRequests Maximum requests within time window + * @param timeWindow Time window size (milliseconds) + */ + constructor(maxRequests: number = 20, timeWindow: number = 60000) { + this.maxRequests = maxRequests + this.timeWindow = timeWindow + } + + /** + * Check if request can be sent + */ + canRequest(): boolean { + this.cleanOldRequests() + return this.requestHistory.length < this.maxRequests + } + + /** + * Record a request + */ + recordRequest(): void { + this.requestHistory.push(Date.now()) + } + + /** + * Get wait time until next available request (milliseconds) + * Returns 0 if request can be sent now + */ + getWaitTime(): number { + if (this.canRequest()) { + return 0 + } + + const oldestRequest = this.requestHistory[0] + return oldestRequest + this.timeWindow - Date.now() + } + + /** + * Clean expired request records + */ + private cleanOldRequests(): void { + const now = Date.now() + const cutoff = now - this.timeWindow + this.requestHistory = this.requestHistory.filter(time => time >= cutoff) + } + + /** + * Wait until request can be sent + */ + async waitUntilReady(): Promise { + const waitTime = this.getWaitTime() + if (waitTime > 0) { + await new Promise(resolve => setTimeout(resolve, waitTime)) + } + this.recordRequest() + } +} diff --git a/services/twitter-services/src/utils/selectors.ts b/services/twitter-services/src/utils/selectors.ts new file mode 100644 index 00000000..01fdb945 --- /dev/null +++ b/services/twitter-services/src/utils/selectors.ts @@ -0,0 +1,83 @@ +/** + * Twitter website CSS selector constants + * Used to locate elements on the page + */ +export const SELECTORS = { + LOGIN: { + USERNAME_INPUT: 'input[autocomplete="username"]', + PASSWORD_INPUT: 'input[type="password"]', + NEXT_BUTTON: 'div[role="button"]:has-text("Next")', + LOGIN_BUTTON: 'div[role="button"]:has-text("Log in")', + NEXT_BUTTON_ALT: '[data-testid="login-next-button"]', + LOGIN_BUTTON_ALT: '[data-testid="login-submit-button"]', + }, + HOME: { + TIMELINE: '[data-testid="primaryColumn"]', + TRENDING: '[data-testid="sidebarColumn"]', + TWEET_COMPOSER: '[data-testid="tweetButtonInline"]', + }, + TIMELINE: { + TWEET: '[data-testid="tweet"]', + TWEET_TEXT: '[data-testid="tweetText"]', + TWEET_TIME: 'time', + LIKE_BUTTON: '[data-testid="like"]', + RETWEET_BUTTON: '[data-testid="retweet"]', + REPLY_BUTTON: '[data-testid="reply"]', + AUTHOR_LINK: '[data-testid="User-Name"] a', + AUTHOR_NAME: '[data-testid="User-Name"] a div span', + AUTHOR_USERNAME: '[data-testid="User-Name"] a div div span', + TWEET_STATS: '[data-testid="tweet"] [role="group"]', + REPLY_COUNT: '[data-testid="reply"] span span', + RETWEET_COUNT: '[data-testid="retweet"] span span', + LIKE_COUNT: '[data-testid="like"] span span', + VIEW_COUNT: '[data-testid="tweet"] [data-testid="app-text-transition-container"] span span', + MEDIA_CONTAINER: '[data-testid="tweetPhoto"]', + VIDEO_CONTAINER: '[data-testid="videoPlayer"]', + TWEET_LINK: 'a[aria-label*="posted"]', + TWEET_ID_CONTAINER: 'a[href*="/status/"]', + }, + PROFILE: { + FOLLOW_BUTTON: '[data-testid="followButton"]', + UNFOLLOW_BUTTON: '[data-testid="unfollowButton"]', + DISPLAY_NAME: '[data-testid="UserName"] div span', + USERNAME: '[data-testid="UserName"] div span:has-text("@")', + BIO: '[data-testid="UserDescription"]', + STATS: '[data-testid="UserProfileStats"]', + FOLLOWING_STAT: '[href$="/following"]', + FOLLOWERS_STAT: '[href$="/followers"]', + AVATAR: '[data-testid="UserAvatar-Container"] img', + BANNER: '[data-testid="UserProfileHeader_Items"] img', + JOIN_DATE: '[data-testid="UserProfileHeader_Items"] span:has-text("Joined")', + LOCATION: '[data-testid="UserProfileHeader_Items"] span:has-text("Location")', + WEBSITE: '[data-testid="UserProfileHeader_Items"] a[href^="https"]', + VERIFIED_BADGE: '[data-testid="icon-verified"]', + }, + COMPOSE: { + TWEET_INPUT: '[data-testid="tweetTextarea_0"]', + TWEET_BUTTON: '[data-testid="tweetButtonInline"]', + MEDIA_BUTTON: '[data-testid="imageOrGifButton"]', + POLL_BUTTON: '[data-testid="createPollButton"]', + EMOJI_BUTTON: '[data-testid="emojiButton"]', + SCHEDULE_BUTTON: '[data-testid="scheduleButton"]', + }, + SEARCH: { + INPUT: '[data-testid="SearchBox_Search_Input"]', + RESULT_TAB: '[role="tablist"] [role="presentation"]', + PEOPLE_TAB: '[role="tab"]:has-text("People")', + LATEST_TAB: '[role="tab"]:has-text("Latest")', + TOP_TAB: '[role="tab"]:has-text("Top")', + SEARCH_FILTERS: '[data-testid="searchFiltersButton"]', + }, + NOTIFICATIONS: { + ITEM: '[data-testid="cellInnerDiv"]', + MENTION: '[data-testid="notification"]', + FOLLOW: '[data-testid="notification"]', + LIKE: '[data-testid="notification"]', + RETWEET: '[data-testid="notification"]', + }, + MESSAGES: { + CONVERSATION: '[data-testid="conversation"]', + MESSAGE_INPUT: '[data-testid="dmComposerTextInput"]', + SEND_BUTTON: '[data-testid="dmComposerSendButton"]', + }, +} diff --git a/services/twitter-services/tsconfig.json b/services/twitter-services/tsconfig.json new file mode 100644 index 00000000..b7208af9 --- /dev/null +++ b/services/twitter-services/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "moduleDetection": "auto", + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts" + ] +}