diff --git a/packages/discord/.env.example b/packages/discord/.env.example new file mode 100644 index 0000000000..4753c32590 --- /dev/null +++ b/packages/discord/.env.example @@ -0,0 +1,30 @@ +# Discord +DISCORD_TOKEN= +DATABASE_URL= # Neon Postgres connection string +ALLOWED_CHANNEL_IDS= # Comma-separated Discord channel IDs +DISCORD_ROLE_ID= # Role ID that triggers the bot (optional, for @role mentions) +DISCORD_CATEGORY_ID= # Optional category ID that is allowed +DISCORD_REQUIRED_ROLE_ID= # Optional role required to talk to bot + +# Daytona +DAYTONA_API_KEY= + +# OpenCode (injected into sandboxes) +OPENCODE_ZEN_API_KEY= +GITHUB_TOKEN= # Optional; enables authenticated gh CLI inside sandbox + +# Observability +LOG_LEVEL=info +LOG_PRETTY=false +HEALTH_HOST=0.0.0.0 +HEALTH_PORT=8787 +TURN_ROUTING_MODE=ai # off | heuristic | ai +TURN_ROUTING_MODEL=claude-haiku-4-5 + +# Bot behavior +SANDBOX_REUSE_POLICY=resume_preferred +SANDBOX_TIMEOUT_MINUTES=30 +PAUSED_TTL_MINUTES=180 +RESUME_HEALTH_TIMEOUT_MS=120000 +SANDBOX_CREATION_TIMEOUT=180 +OPENCODE_MODEL=opencode/claude-sonnet-4-5 diff --git a/packages/discord/.gitignore b/packages/discord/.gitignore new file mode 100644 index 0000000000..f7ab915e48 --- /dev/null +++ b/packages/discord/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local +.env.*.local + +# Bun +*.lockb + +# Turbo +.turbo/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Sensitive +.censitive diff --git a/packages/discord/AGENTS.md b/packages/discord/AGENTS.md new file mode 100644 index 0000000000..55e74909ae --- /dev/null +++ b/packages/discord/AGENTS.md @@ -0,0 +1,177 @@ +# AGENTS.md + +Guide for coding agents working in this repository. +Use this file for build/test commands and coding conventions. + +## Project Snapshot +- Stack: Bun + TypeScript (ESM, strict mode) +- App: Discord bot that provisions Daytona sandboxes +- Persistence: Neon Postgres (`discord_sessions`) +- Runtime flow: Discord thread -> sandbox -> OpenCode session +- Ops: structured JSON logs + `/healthz` and `/readyz` + +## Repository Map +- `src/index.ts`: startup, wiring, graceful shutdown +- `src/config.ts`: env schema and parsing (Zod) +- `src/discord/`: Discord client + handlers + routing logic +- `src/sandbox/`: sandbox lifecycle + OpenCode transport +- `src/sessions/store.ts`: Neon-backed session store +- `src/db/init.ts`: idempotent DB schema initialization +- `src/http/health.ts`: health/readiness HTTP server +- `.env.example`: env contract + +## Setup and Run Commands +### Install +- `bun install` + +### First-time local setup +- `cp .env.example .env` +- Fill required secrets in `.env` +- Initialize schema: `bun run db:init` + +### Development run +- Watch mode: `bun run dev` +- Normal run: `bun run start` +- Dev bootstrap helper: `bun run dev:setup` + +### Static checks +- Typecheck: `bun run typecheck` +- Build: `bun run build` +- Combined check: `bun run check` + +### Health checks +- `curl -s http://127.0.0.1:8787/healthz` +- `curl -i http://127.0.0.1:8787/readyz` + +## Testing Commands +There is no first-party test suite in `src/` yet. +Use Bun test commands for new tests. +- Run all tests (if present): `bun test` +- Run a single test file: `bun test path/to/file.test.ts` +- Run one file in watch mode: `bun test --watch path/to/file.test.ts` +When adding tests, prefer colocated `*.test.ts` near implementation files. + +## Cursor / Copilot Rules +Checked these paths: +- `.cursor/rules/` +- `.cursorrules` +- `.github/copilot-instructions.md` +No Cursor/Copilot rule files currently exist in this repo. +If added later, update this file and follow those rules. + +## Code Style +### TypeScript and modules +- Keep code strict-TypeScript compatible. +- Use ESM imports/exports only. +- Prefer named exports over default exports. +- Add explicit return types on exported functions. + +### Imports +- Group imports as: external first, then internal. +- Use `import type` for type-only imports. +- Keep import paths consistent with existing relative style. + +### Formatting +- Match existing style: + - double quotes + - semicolons + - trailing commas where appropriate +- Keep functions small and focused. +- Avoid comments unless logic is non-obvious. + +### Naming +- `camelCase`: variables/functions +- `PascalCase`: classes/interfaces/type aliases +- `UPPER_SNAKE_CASE`: env keys and constants +- Log events should be stable (`domain.action.result`). + +### Types and contracts +- Reuse shared types from `src/types.ts`. +- Preserve `SessionStatus` semantics when adding new states. +- Prefer `unknown` over `any` at untrusted boundaries. +- Narrow and validate external data before use. + +## Error Handling and Logging +- Use `logger` from `src/observability/logger.ts`. +- Do not add raw `console.log` in app paths. +- Include context fields when available: + - `threadId` + - `channelId` + - `guildId` + - `sandboxId` + - `sessionId` +- Fail fast on invalid config in `src/config.ts`. +- Wrap network/process operations in contextual `try/catch`. +- Separate recoverable errors from terminal errors. +- Never log secret values. + +## Environment and Secrets +- Read env only through `getEnv()`. +- Update `.env.example` for env schema changes. +- Keep auth tokens out of command strings and logs. +- Pass runtime secrets via environment variables. + +## Domain-Specific Rules +### Session lifecycle +- Neon mapping (`thread_id`, `sandbox_id`, `session_id`) is authoritative. +- Resume existing sandbox/session before creating replacements. +- Recreate only when sandbox is unavailable/destroyed. +- If session changes, replay Discord thread history as fallback context. + +### Daytona behavior +- `stop()` clears running processes but keeps filesystem state. +- `start()` requires process bootstrap (`opencode serve`). +- Keep lifecycle transitions deterministic and observable. + +### OpenCode transport +- Keep preview token separate from persisted URL when possible. +- Send token using `x-daytona-preview-token` header. +- Keep retry loops bounded and configurable. + +### Discord handler behavior +- Ignore bot/self chatter and respect mention/role gating. +- Preserve thread ownership checks for bot-managed threads. +- Keep outbound messages chunked for Discord size limits. + +## Non-Obvious Discoveries + +### OpenCode session persistence +- Sessions are disk-persistent JSON files in `~/.local/share/opencode/storage/session//` +- Sessions survive `opencode serve` restarts if filesystem intact AND process restarts from same git repo directory +- Sessions are scoped by `projectID` = git root commit hash (or `"global"` for non-git dirs) +- After `daytona.start()`, processes are guaranteed dead - always restart `opencode serve` immediately, don't wait for health first (`src/sandbox/manager.ts:400-420`) + +### Session reattach debugging +- If `sessionExists()` returns false but sandbox filesystem is intact, search by title (`Discord thread `) via `listSessions()` - session may exist under different ID due to OpenCode internal state changes +- Thread lock per `threadId` prevents concurrent create/resume races (`src/sandbox/manager.ts:614-632`) +- Never fall back to new sandbox when `daytona.start()` succeeds - filesystem is intact, only OpenCode process needs restart + +### Discord + multiple processes +- Multiple bot processes with same `DISCORD_TOKEN` cause race conditions - one succeeds, others fail with `DiscordAPIError[160004]` (thread already created) +- PTY sessions with `exec bash -l` stay alive after command exits, leading to duplicate bot runtimes if not cleaned up + +### Sandbox runtime auth +- Pass `GITHUB_TOKEN` as process env to `opencode serve` via `sandbox.process.executeCommand()` `env` parameter +- Never interpolate tokens into command strings - use `env` parameter in `exec()` options (`src/sandbox/manager.ts:27-72`) + +## Agent Workflow Checklist +### Before coding +- Read related modules and follow existing patterns. +- Prefer narrow, minimal changes over broad refactors. + +### During coding +- Keep behavior backwards-compatible unless intentionally changing it. +- Keep changes cohesive (schema + store + manager together). +- Add/update logs for important lifecycle branches. + +### After coding +- Run `bun run typecheck` +- Run `bun run build` +- Run `bun run db:init` for schema-affecting changes +- Smoke-check health endpoints if bootstrap/runtime changed + +## Git/PR Safety for Agents +- Do not commit or push unless explicitly requested. +- Do not amend commits unless explicitly requested. +- Avoid destructive git commands unless explicitly requested. +- Summaries should cite changed files and operational impact. diff --git a/packages/discord/Dockerfile b/packages/discord/Dockerfile new file mode 100644 index 0000000000..bc69f9be59 --- /dev/null +++ b/packages/discord/Dockerfile @@ -0,0 +1,24 @@ +# Stage 1: Install dependencies +FROM oven/bun:1.2-alpine AS deps +WORKDIR /app +COPY package.json bun.lock* ./ +COPY web/package.json web/ +COPY discord-bot/package.json discord-bot/ +RUN bun install --frozen-lockfile + +# Stage 2: Production image +FROM oven/bun:1.2-alpine AS runner +WORKDIR /app + +COPY package.json bun.lock* ./ +COPY web/package.json web/ +COPY discord-bot/package.json discord-bot/ +RUN bun install --production --frozen-lockfile + +# Copy shared DB schema (discord-bot imports from main project) +COPY src/db/schema.ts src/db/schema.ts + +# Copy discord bot source +COPY discord-bot/src/ discord-bot/src/ + +CMD ["bun", "run", "discord-bot/src/index.ts"] diff --git a/packages/discord/README.md b/packages/discord/README.md new file mode 100644 index 0000000000..c8ef92d809 --- /dev/null +++ b/packages/discord/README.md @@ -0,0 +1,59 @@ +# OpenCord + +Discord bot that provisions [Daytona](https://daytona.io) sandboxes running [OpenCode](https://opencode.ai) sessions. Each Discord thread gets its own isolated sandbox with full conversational context. + +## How It Works + +1. Mention the bot in an allowed channel +2. Bot creates a Discord thread and provisions a Daytona sandbox +3. OpenCode runs inside the sandbox, responding to messages in the thread +4. Inactive threads pause their sandbox automatically; activity resumes them +5. Conversational context is preserved across bot restarts + +## Quick Start + +```bash +bun install +cp .env.example .env +# Fill in required values in .env +bun run db:init +bun run dev +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `bun run dev` | Watch mode | +| `bun run start` | Production run | +| `bun run db:init` | Initialize/migrate database | +| `bun run typecheck` | TypeScript checks | +| `bun run build` | Bundle for deployment | +| `bun run check` | Typecheck + build | + +## Configuration + +See [`.env.example`](.env.example) for all available environment variables. Required: + +- `DISCORD_TOKEN` — Discord bot token +- `DATABASE_URL` — Neon Postgres connection string +- `DAYTONA_API_KEY` — Daytona API key +- `OPENCODE_ZEN_API_KEY` — OpenCode API key + +## Health Endpoints + +- `GET /healthz` — Liveness check (uptime, Discord status, active sessions) +- `GET /readyz` — Readiness check (200 when Discord connected, 503 otherwise) + +## Architecture + +``` +Discord thread + └─ message-create handler + └─ SandboxManager.resolveSessionForMessage() + ├─ active? → health check → reuse + ├─ paused? → daytona.start() → restart opencode → reattach session + └─ missing? → create sandbox → clone repo → start opencode → new session +``` + +Sessions are persisted in Neon Postgres. Sandbox filesystem (including OpenCode session state) survives pause/resume cycles via Daytona stop/start. diff --git a/packages/discord/bun.lock b/packages/discord/bun.lock new file mode 100644 index 0000000000..6619ef66aa --- /dev/null +++ b/packages/discord/bun.lock @@ -0,0 +1,625 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "discord-bot", + "dependencies": { + "@daytonaio/sdk": "latest", + "@neondatabase/serverless": "^0.10", + "@opencode-ai/sdk": "latest", + "discord.js": "^14", + "effect": "^3", + "zod": "^3", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22", + "typescript": "^5", + }, + }, + }, + "packages": { + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.988.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.8", "@aws-sdk/credential-provider-node": "^3.972.7", "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", "@aws-sdk/middleware-expect-continue": "^3.972.3", "@aws-sdk/middleware-flexible-checksums": "^3.972.6", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-location-constraint": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-sdk-s3": "^3.972.8", "@aws-sdk/middleware-ssec": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/signature-v4-multi-region": "3.988.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.30", "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-mt7AdkieJJ5hEKeCxH4sdTTd679shUjo/cUvNY0fUHgQIPZa1jRuekTXnRytRrEwdrZWJDx56n1S8ism2uX7jg=="], + + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.988.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.8", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.30", "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ThqQ7aF1k0Zz4yJRwegHw+T1rM3a7ZPvvEUSEdvn5Z8zTeWgJAbtqW/6ejPsMLmFOlHgNcwDQN/e69OvtEOoIQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.973.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", "@smithy/core": "^3.23.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-WeYJ2sfvRLbbUIrjGMUXcEHGu5SJk53jz3K9F8vFP42zWyROzPJ2NB6lMu9vWl5hnMwzwabX7pJc9Euh3JyMGw=="], + + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-+dYEBWgTqkQQHFUllvBL8SLyXyLKWdxLMD1LmKJRvmb0NMJuaJFG/qg78C+LE67eeGbipYcE+gJ48VlLBGHlMw=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-z3QkozMV8kOFisN2pgRag/f0zPDrw96mY+ejAM0xssV/+YQ2kklbylRNI/TcTQUDnGg0yPxNjyV6F2EM2zPTwg=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/credential-provider-env": "^3.972.6", "@aws-sdk/credential-provider-http": "^3.972.8", "@aws-sdk/credential-provider-login": "^3.972.6", "@aws-sdk/credential-provider-process": "^3.972.6", "@aws-sdk/credential-provider-sso": "^3.972.6", "@aws-sdk/credential-provider-web-identity": "^3.972.6", "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6tkIYFv3sZH1XsjQq+veOmx8XWRnyqTZ5zx/sMtdu/xFRIzrJM1Y2wAXeCJL1rhYSB7uJSZ1PgALI2WVTj78ow=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LXsoBoaTSGHdRCQXlWSA0CHHh05KWncb592h9ElklnPus++8kYn1Ic6acBR4LKFQ0RjjMVgwe5ypUpmTSUOjPA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.7", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.6", "@aws-sdk/credential-provider-http": "^3.972.8", "@aws-sdk/credential-provider-ini": "^3.972.6", "@aws-sdk/credential-provider-process": "^3.972.6", "@aws-sdk/credential-provider-sso": "^3.972.6", "@aws-sdk/credential-provider-web-identity": "^3.972.6", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PuJ1IkISG7ZDpBFYpGotaay6dYtmriBYuHJ/Oko4VHxh8YN5vfoWnMNYFEWuzOfyLmP7o9kDVW0BlYIpb3skvw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Yf34cjIZJHVnD92jnVYy3tNjM+Q4WJtffLK2Ehn0nKpZfqd1m7SI0ra22Lym4C53ED76oZENVSS2wimoXJtChQ=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.6", "", { "dependencies": { "@aws-sdk/client-sso": "3.988.0", "@aws-sdk/core": "^3.973.8", "@aws-sdk/token-providers": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-2+5UVwUYdD4BBOkLpKJ11MQ8wQeyJGDVMDRH5eWOULAh9d6HJq07R69M/mNNMC9NTjr3mB1T0KGDn4qyQh5jzg=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-pdJzwKtlDxBnvZ04pWMqttijmkUIlwOsS0GcxCjzEVyUMpARysl0S0ks74+gs2Pdev3Ujz+BTAjOc1tQgAxGqA=="], + + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.988.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/smithy-client": "^4.11.3", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.988.0" } }, "sha512-EZ6US/oV1n7WtvVZ5FR80g8pA7xU92lBsvjibANVm/uHr8FOqPmSqQV9KvwzRmSdp0O6J1kXxi9LqmwgJVlN/A=="], + + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg=="], + + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg=="], + + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.972.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.8", "@aws-sdk/crc64-nvme": "3.972.0", "@aws-sdk/types": "^3.973.1", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-g5DadWO58IgQKuq+uLL3pLohOwLiA67gB49xj8694BW+LpHLNu/tjCqwLfIaWvZyABbv0LXeNiiTuTnjdgkZWw=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="], + + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="], + + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/core": "^3.23.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-/yJdahpN/q3Dc88qXBTQVZfnXryLnxfCoP4hGClbKjuF0VCMxrz3il7sj0GhIkEQt5OM5+lA88XrvbjjuwSxIg=="], + + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.988.0", "@smithy/core": "^3.23.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-3PGL+Kvh1PhB0EeJeqNqOWQgipdqFheO4OUKc6aYiFwEpM5t9AyE5hjjxZ5X6iSj8JiduWFZLPwASzF6wQRgFg=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.988.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.8", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.30", "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OgYV9k1oBCQ6dOM+wWAMNNehXA8L4iwr7ydFV+JDHyuuu0Ko7tDXnLEtEmeQGYRcAFU3MGasmlBkMB8vf4POrg=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.988.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.8", "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-SXwhbe2v0Jno7QLIBmZWAL2eVzGmXkfLLy0WkM6ZJVhE0SFUcnymDwMUA1oMDUvyArzvKBiU8khQ2ImheCKOHQ=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.988.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.8", "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-xvXVlRVKHnF2h6fgWBm64aPP5J+58aJyGfRrQa/uFh8a9mcK68mLfJOYq+ZSxQy/UN3McafJ2ILAy7IWzT9kRw=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.988.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.4", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.6", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-966xH8TPqkqOXP7EwnEThcKKz0SNP9kVJBKd9M8bNXE4GSqVouMKKnFBwYnzbWVKuLXubzX5seokcX4a0JLJIA=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.4", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" } }, "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + + "@daytonaio/api-client": ["@daytonaio/api-client@0.141.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-DSPCurIEjfFyXCd07jkDgfsoFppVhTLyIJdvfb0LgG1EgV75BPqqzk2WM4ragBFJUuK2URF5CK7qkaHW0AXKMA=="], + + "@daytonaio/sdk": ["@daytonaio/sdk@0.141.0", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytonaio/api-client": "0.141.0", "@daytonaio/toolbox-api-client": "0.141.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.207.0", "@opentelemetry/instrumentation-http": "^0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "axios": "^1.13.5", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.7" } }, "sha512-JUopkS9SkO7h4WN8CjparOrP9k954euOF5KG//PeCEFOxUWTPFOME70GrmHXQKa1qkdZiF/4tz9jtZ744B1I2w=="], + + "@daytonaio/toolbox-api-client": ["@daytonaio/toolbox-api-client@0.141.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-KGkCLDLAltd9FCic3PhSJGrTp3RwGsUwWEGp5vyWZFQGWpJV8CVp08CH5SBdo4YhuqFUVlyQcwha1HpzpVH++A=="], + + "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@neondatabase/serverless": ["@neondatabase/serverless@0.10.4", "", { "dependencies": { "@types/pg": "8.11.6" } }, "sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.60", "", {}, "sha512-RVbQQCXLr1yXCYnlYiIF0t4lMUPCh4x2EU/FfvnfiDaBAwKOoIvQUjZd+AXSpjbConum6Wa2PzV65waFhtix1w=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.2.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JpOh7MguEUls8eRfkVVW3yRhClo5b9LqwWTOg8+i4gjr/+8eiCtquJnC7whvpTIGyff06cLZ2NsEj+CVP3Mjeg=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RQJEV/K6KPbQrIUbsrRkEe0ufks1o5OGLHy6jbDD8tRjeCsbFHWfg99lYBRqBV33PYZJXsigqMaAbjWGTFYzLw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6flX89W54gkwmqYShdcTBR1AEF5C1Ob0O8pDgmLPikTKyEv27lByr9yBmO5WrP0+5qJuNPHrLfgFQFYi6npDGA=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fG8FAJmvXOrKXGIRN8+y41U41IfVXxPRVwyB05LoMqYSjugx/FSBkMZUZXUT/wclTdmBKtS5MKoi0bEKkmRhSw=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kDBxiTeQjaRlUQzS1COT9ic+et174toZH6jxaVuVAvGqmxOkgjpLOjrI5ff8SMMQE69r03L3Ll3nPKekLopLwg=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y5p1s39FvIRmU+F1++j7ly8/KSqhMmn6cMfpQqiDCqDjdDHwUtSq0XI0WwL3HYGnZeaR/VV4BNmsYQJ7GAPrhw=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7u2ZmcIx6D4KG/+5np4X2qA0o+O0K8cnUDhR4WI/vr5ZZ0la9J9RG+tkSjC7Yz+2XgL6760gSIM7/nyd3yaBLA=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ruUQB4FkWtxHjNmSXjrhmJZFvyMm+tBzHyMm7YPQshApy4wvZUTcrpPyP/A/rCl/8M4BwoVIZdiwijMdbZaq4w=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FC4i5hVixTzuhg4SV2ycTEAYx+0E2hm+GwbdoVPSA6kna0pPVI4etzaA9UkpJ9ussumQheFXP6rkGIaFJjMxsw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="], + + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], + + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.207.0", "@opentelemetry/exporter-logs-otlp-http": "0.207.0", "@opentelemetry/exporter-logs-otlp-proto": "0.207.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.207.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0", "@opentelemetry/exporter-prometheus": "0.207.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.207.0", "@opentelemetry/exporter-trace-otlp-http": "0.207.0", "@opentelemetry/exporter-trace-otlp-proto": "0.207.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.2.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], + + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="], + + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], + + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="], + + "@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.8", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA=="], + + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.9", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA=="], + + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@smithy/md5-js": ["@smithy/md5-js@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.10", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0" } }, "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.3", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="], + + "@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.8", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.12", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "@types/pg": ["@types/pg@8.11.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="], + + "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="], + + "dotenv": ["dotenv@17.2.4", "", {}, "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "effect": ["effect@3.19.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-7+XC3vGrbAhCHd8LTFHvnZjRpZKZ8YHRZqJTkpNoxcJ2mCyNs2SwI+6VkV/ij8Y3YW7wfBN4EbU06/F5+m/wkQ=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="], + + "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + + "pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], + + "postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="], + + "postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="], + + "postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="], + + "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], + + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + } +} diff --git a/packages/discord/package.json b/packages/discord/package.json new file mode 100644 index 0000000000..d930a56fb8 --- /dev/null +++ b/packages/discord/package.json @@ -0,0 +1,28 @@ +{ + "name": "discord-bot", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "db:init": "bun run src/db/init.ts", + "dev": "bun run --watch src/index.ts", + "dev:setup": "bun run db:init", + "start": "bun run src/index.ts", + "build": "bun build src/index.ts --target=bun --outdir=dist", + "typecheck": "tsc --noEmit", + "check": "bun run typecheck && bun run build" + }, + "dependencies": { + "discord.js": "^14", + "@neondatabase/serverless": "^0.10", + "@daytonaio/sdk": "latest", + "@opencode-ai/sdk": "latest", + "effect": "^3", + "zod": "^3" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22", + "typescript": "^5" + } +} diff --git a/packages/discord/src/agent-prompt.md b/packages/discord/src/agent-prompt.md new file mode 100644 index 0000000000..da5c77f11b --- /dev/null +++ b/packages/discord/src/agent-prompt.md @@ -0,0 +1,28 @@ +You're a senior engineer on the OpenCode team. You're in a Discord channel where teammates and community members ask questions about the codebase. You have the full opencode repo cloned at your working directory. + +This is an internal tool — people tag you to ask about how things work, where code lives, why something was built a certain way, or to get help debugging. Think of it like someone pinging you on Slack. + +## Tone + +- Just answer the question. Don't preface with "Based on my analysis" or "I'd be happy to help" or "Let me look into that for you." Just give the answer. +- Write like you're messaging a coworker. Lowercase is fine. Short paragraphs. No essays. +- Don't over-format. Use markdown for code blocks and the occasional list, but don't turn every response into a formatted document with headers and bullet points. Just talk. +- Be direct and opinionated when it makes sense. "yeah that's a bug" or "I'd just use X here" is better than hedging everything. +- If you don't know, say "not sure" or "I'd have to dig into that more." Don't make stuff up. +- Match the vibe. Quick question = quick answer. Detailed question = longer answer with code refs. + +## What you do + +- Search and read the codebase to answer questions +- Run git, grep, gh CLI to find things +- Reference specific files and line numbers like `src/tui/app.ts:142` +- Quote relevant code when it helps +- Explain architecture and design decisions based on what's actually in the code + +## Rules + +- **Search the code first.** Don't answer from memory — look it up and cite where things are. +- **Don't edit files unless someone explicitly asks you to.** +- **Keep it short.** Under 1500 chars unless the question actually needs a longer answer. +- **Summarize command output.** Don't paste raw terminal dumps. +- When you reference code, include the file path so people can go look at it. diff --git a/packages/discord/src/config.ts b/packages/discord/src/config.ts new file mode 100644 index 0000000000..dba9d7018f --- /dev/null +++ b/packages/discord/src/config.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +const envSchema = z.object({ + DISCORD_TOKEN: z.string().min(1), + ALLOWED_CHANNEL_IDS: z.string().default("").transform((s) => + s + .split(",") + .map((id) => id.trim()) + .filter((id) => id.length > 0), + ), + DISCORD_CATEGORY_ID: z.string().default(""), + DISCORD_ROLE_ID: z.string().default(""), + DISCORD_REQUIRED_ROLE_ID: z.string().default(""), + DATABASE_URL: z.string().min(1), + DAYTONA_API_KEY: z.string().min(1), + OPENCODE_ZEN_API_KEY: z.string().min(1), + GITHUB_TOKEN: z.string().default(""), + LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), + LOG_PRETTY: z + .string() + .default("false") + .transform((value) => value.toLowerCase() === "true"), + HEALTH_HOST: z.string().default("0.0.0.0"), + HEALTH_PORT: z.coerce.number().default(8787), + TURN_ROUTING_MODE: z.enum(["off", "heuristic", "ai"]).default("ai"), + TURN_ROUTING_MODEL: z.string().default("claude-haiku-4-5"), + SANDBOX_REUSE_POLICY: z.enum(["resume_preferred", "recreate"]).default("resume_preferred"), + SANDBOX_TIMEOUT_MINUTES: z.coerce.number().default(30), + PAUSED_TTL_MINUTES: z.coerce.number().default(180), + RESUME_HEALTH_TIMEOUT_MS: z.coerce.number().default(120000), + SANDBOX_CREATION_TIMEOUT: z.coerce.number().default(180), + OPENCODE_MODEL: z.string().default("opencode/claude-sonnet-4-5"), +}); + +export type Env = z.infer; + +let _config: Env | null = null; + +export function getEnv(): Env { + if (!_config) { + const result = envSchema.safeParse(process.env); + if (!result.success) { + console.error(JSON.stringify({ + ts: new Date().toISOString(), + level: "error", + event: "config.invalid", + component: "config", + message: "Invalid environment variables", + fieldErrors: result.error.flatten().fieldErrors, + })); + throw new Error("Invalid environment configuration"); + } + _config = result.data; + } + return _config; +} diff --git a/packages/discord/src/db/client.ts b/packages/discord/src/db/client.ts new file mode 100644 index 0000000000..fdefce7294 --- /dev/null +++ b/packages/discord/src/db/client.ts @@ -0,0 +1,11 @@ +import { neon } from "@neondatabase/serverless"; +import { getEnv } from "../config"; + +let _sql: ReturnType | null = null; + +export function getSql() { + if (!_sql) { + _sql = neon(getEnv().DATABASE_URL); + } + return _sql; +} diff --git a/packages/discord/src/db/init.ts b/packages/discord/src/db/init.ts new file mode 100644 index 0000000000..a255297765 --- /dev/null +++ b/packages/discord/src/db/init.ts @@ -0,0 +1,65 @@ +import { getSql } from "./client"; +import { logger } from "../observability/logger"; + +const PREFIX = "[db]"; + +export async function initializeDatabase(): Promise { + const sql = getSql(); + + await sql`CREATE TABLE IF NOT EXISTS discord_sessions ( + thread_id TEXT PRIMARY KEY, + channel_id TEXT NOT NULL, + guild_id TEXT NOT NULL, + sandbox_id TEXT NOT NULL, + session_id TEXT NOT NULL, + preview_url TEXT NOT NULL, + preview_token TEXT, + status TEXT NOT NULL CHECK (status IN ('creating', 'active', 'pausing', 'paused', 'resuming', 'destroying', 'destroyed', 'error')), + last_activity TIMESTAMPTZ NOT NULL DEFAULT NOW(), + pause_requested_at TIMESTAMPTZ, + paused_at TIMESTAMPTZ, + resume_attempted_at TIMESTAMPTZ, + resumed_at TIMESTAMPTZ, + destroyed_at TIMESTAMPTZ, + last_health_ok_at TIMESTAMPTZ, + last_error TEXT, + resume_fail_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + )`; + + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS preview_token TEXT`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS last_activity TIMESTAMPTZ NOT NULL DEFAULT NOW()`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS pause_requested_at TIMESTAMPTZ`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS paused_at TIMESTAMPTZ`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS resume_attempted_at TIMESTAMPTZ`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS resumed_at TIMESTAMPTZ`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS destroyed_at TIMESTAMPTZ`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS last_health_ok_at TIMESTAMPTZ`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS last_error TEXT`; + await sql`ALTER TABLE discord_sessions ADD COLUMN IF NOT EXISTS resume_fail_count INTEGER NOT NULL DEFAULT 0`; + + await sql`ALTER TABLE discord_sessions DROP CONSTRAINT IF EXISTS discord_sessions_status_check`; + await sql`ALTER TABLE discord_sessions + ADD CONSTRAINT discord_sessions_status_check + CHECK (status IN ('creating', 'active', 'pausing', 'paused', 'resuming', 'destroying', 'destroyed', 'error'))`; + + await sql`CREATE INDEX IF NOT EXISTS discord_sessions_status_last_activity_idx + ON discord_sessions (status, last_activity)`; + + await sql`CREATE INDEX IF NOT EXISTS discord_sessions_status_updated_at_idx + ON discord_sessions (status, updated_at)`; +} + +if (import.meta.main) { + initializeDatabase() + .then(() => { + logger.info({ event: "db.schema.ready", component: "db", message: `${PREFIX} Schema is ready` }); + }) + .catch((err) => { + logger.error({ event: "db.schema.failed", component: "db", message: `${PREFIX} Failed to initialize schema`, error: err }); + process.exit(1); + }); +} diff --git a/packages/discord/src/discord/client.ts b/packages/discord/src/discord/client.ts new file mode 100644 index 0000000000..9cc3c0467d --- /dev/null +++ b/packages/discord/src/discord/client.ts @@ -0,0 +1,14 @@ +import { Client, GatewayIntentBits, Partials } from "discord.js"; + +export function createDiscordClient(): Client { + const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], + partials: [Partials.Channel], + }); + + return client; +} diff --git a/packages/discord/src/discord/format.ts b/packages/discord/src/discord/format.ts new file mode 100644 index 0000000000..534d56b720 --- /dev/null +++ b/packages/discord/src/discord/format.ts @@ -0,0 +1,70 @@ +const MAX_MESSAGE_LENGTH = 1900; + +/** + * Splits a long response into Discord-safe message chunks (<2000 chars). + * Splits at code block boundaries, paragraph breaks, or sentence ends. + * Handles unclosed code blocks across chunks. + */ +export function splitForDiscord(text: string): string[] { + if (!text || text.length === 0) return ["(No response)"]; + if (text.length <= MAX_MESSAGE_LENGTH) return [text]; + + const messages: string[] = []; + let remaining = text; + + while (remaining.length > 0) { + if (remaining.length <= MAX_MESSAGE_LENGTH) { + messages.push(remaining); + break; + } + + // Find best split point + let splitAt = -1; + + // Prefer splitting at end of code block + splitAt = remaining.lastIndexOf("\n```\n", MAX_MESSAGE_LENGTH); + if (splitAt !== -1) splitAt += 4; // include the closing ```\n + + // Then paragraph break + if (splitAt === -1 || splitAt < MAX_MESSAGE_LENGTH / 2) { + const paraBreak = remaining.lastIndexOf("\n\n", MAX_MESSAGE_LENGTH); + if (paraBreak > MAX_MESSAGE_LENGTH / 2) splitAt = paraBreak; + } + + // Then sentence end + if (splitAt === -1 || splitAt < MAX_MESSAGE_LENGTH / 2) { + const sentenceEnd = remaining.lastIndexOf(". ", MAX_MESSAGE_LENGTH); + if (sentenceEnd > MAX_MESSAGE_LENGTH / 2) splitAt = sentenceEnd + 1; + } + + // Fallback: hard cut + if (splitAt === -1 || splitAt < MAX_MESSAGE_LENGTH / 4) { + splitAt = MAX_MESSAGE_LENGTH; + } + + const chunk = remaining.slice(0, splitAt); + remaining = remaining.slice(splitAt).trimStart(); + + // Handle unclosed code blocks + const backtickCount = (chunk.match(/```/g) || []).length; + if (backtickCount % 2 !== 0) { + // Odd = unclosed code block + messages.push(chunk + "\n```"); + remaining = "```\n" + remaining; + } else { + messages.push(chunk); + } + } + + return messages.filter((m) => m.trim().length > 0); +} + +/** + * Cleans up the response text for Discord. + * Strips any leading/trailing whitespace and limits consecutive newlines. + */ +export function cleanResponse(text: string): string { + return text + .trim() + .replace(/\n{4,}/g, "\n\n\n"); // max 3 consecutive newlines +} diff --git a/packages/discord/src/discord/handlers/message-create.ts b/packages/discord/src/discord/handlers/message-create.ts new file mode 100644 index 0000000000..3615bf45fc --- /dev/null +++ b/packages/discord/src/discord/handlers/message-create.ts @@ -0,0 +1,321 @@ +import type { Client, Message, TextChannel, ThreadChannel, GuildMember } from "discord.js"; +import { ChannelType } from "discord.js"; +import { getEnv } from "../../config"; +import { cleanResponse, splitForDiscord } from "../format"; +import { shouldRespondToOwnedThreadTurn } from "../turn-routing"; +import { generateThreadName } from "../thread-name"; +import type { SandboxManager } from "../../sandbox/manager"; +import { logger } from "../../observability/logger"; + +/** + * Checks if a channel (or its parent for threads) is allowed. + * Allowed means: in the ALLOWED_CHANNEL_IDS list, OR in the DISCORD_CATEGORY_ID category. + */ +function isChannelAllowed(channelId: string, categoryId: string | null, env: ReturnType): boolean { + if (env.ALLOWED_CHANNEL_IDS.length > 0 && env.ALLOWED_CHANNEL_IDS.includes(channelId)) { + return true; + } + if (env.DISCORD_CATEGORY_ID && categoryId === env.DISCORD_CATEGORY_ID) { + return true; + } + return false; +} + +/** + * Checks if a user has the required role (if configured). + */ +function hasRequiredRole(member: GuildMember | null, env: ReturnType): boolean { + if (!env.DISCORD_REQUIRED_ROLE_ID) return true; // no role requirement + if (!member) return false; + return member.roles.cache.has(env.DISCORD_REQUIRED_ROLE_ID); +} + +const HISTORY_FETCH_LIMIT = 40; +const HISTORY_LINE_CHAR_LIMIT = 500; +const HISTORY_TOTAL_CHAR_LIMIT = 6000; + +function normalizeHistoryText(value: string): string { + return value.replace(/\s+/g, " ").trim(); +} + +async function buildHistoryReplayPrompt( + thread: ThreadChannel, + currentMessage: Message, + latestUserContent: string, +): Promise<{ prompt: string; historyCount: number }> { + try { + const fetched = await thread.messages.fetch({ limit: HISTORY_FETCH_LIMIT }); + const ordered = [...fetched.values()].sort((a, b) => a.createdTimestamp - b.createdTimestamp); + + const lines: string[] = []; + for (const prior of ordered) { + if (prior.id === currentMessage.id || prior.system) continue; + + let lineContent = normalizeHistoryText(prior.content); + if (!lineContent && prior.attachments.size > 0) { + const files = [...prior.attachments.values()].map((att) => att.name ?? "file").join(", "); + lineContent = `[attachments: ${files}]`; + } + + if (!lineContent) continue; + if (lineContent.length > HISTORY_LINE_CHAR_LIMIT) { + lineContent = `${lineContent.slice(0, HISTORY_LINE_CHAR_LIMIT)}...`; + } + + const role = prior.author.bot ? "assistant" : "user"; + lines.push(`${role}: ${lineContent}`); + } + + if (lines.length === 0) { + return { prompt: latestUserContent, historyCount: 0 }; + } + + const selected: string[] = []; + let totalChars = 0; + for (let i = lines.length - 1; i >= 0; i -= 1) { + const candidate = lines[i]; + if (totalChars + candidate.length > HISTORY_TOTAL_CHAR_LIMIT && selected.length > 0) break; + selected.unshift(candidate); + totalChars += candidate.length; + } + + return { + prompt: [ + "Conversation history from this same Discord thread (oldest to newest):", + selected.join("\n"), + "", + "Continue the same conversation and respond to the latest user message:", + latestUserContent, + ].join("\n"), + historyCount: selected.length, + }; + } catch { + return { prompt: latestUserContent, historyCount: 0 }; + } +} + +/** + * Creates a messageCreate event handler bound to the given SandboxManager. + */ +export function createMessageHandler(client: Client, sandboxManager: SandboxManager) { + const env = getEnv(); + + return async (message: Message): Promise => { + if (message.author.bot) return; + + // Check if the bot is mentioned (ignore @everyone and @here) + if (message.mentions.everyone) return; + + const botUserId = client.user?.id ?? ""; + const roleId = env.DISCORD_ROLE_ID; + + const mentionedByUser = client.user ? message.mentions.has(client.user, { ignoreEveryone: true, ignoreRoles: false }) : false; + const mentionedByRole = roleId ? message.mentions.roles.has(roleId) : false; + const mentionedInContent = message.content.includes(`<@${botUserId}>`) || message.content.includes(`<@!${botUserId}>`); + const roleMentionInContent = roleId ? message.content.includes(`<@&${roleId}>`) : false; + + const isMentioned = mentionedByUser || mentionedByRole || mentionedInContent || roleMentionInContent; + + // In threads the bot owns, respond to ALL messages (no mention needed) + const isInThread = message.channel.type === ChannelType.PublicThread || message.channel.type === ChannelType.PrivateThread; + let isOwnedThread = false; + if (isInThread && !isMentioned) { + isOwnedThread = await sandboxManager.hasTrackedThread(message.channelId); + + if (isOwnedThread) { + const decision = await shouldRespondToOwnedThreadTurn({ + mode: env.TURN_ROUTING_MODE, + model: env.TURN_ROUTING_MODEL, + apiKey: env.OPENCODE_ZEN_API_KEY, + content: message.content, + botUserId, + botRoleId: roleId, + mentionedUserIds: [...message.mentions.users.keys()], + mentionedRoleIds: [...message.mentions.roles.keys()], + }); + + if (!decision.shouldRespond) { + logger.info({ + event: "discord.message.skipped.not_directed", + component: "message-handler", + message: "Skipped turn not directed at bot", + channelId: message.channelId, + userId: message.author.id, + reason: decision.reason, + }); + return; + } + } + } + + if (!isMentioned && !isOwnedThread) return; + + // Check required role + const member = message.member; + if (!hasRequiredRole(member, env)) { + logger.info({ + event: "discord.message.ignored.role", + component: "message-handler", + message: "Ignored message from user without required role", + channelId: message.channelId, + userId: message.author.id, + }); + return; + } + + logger.info({ + event: "discord.message.triggered", + component: "message-handler", + message: "Bot triggered", + channelId: message.channelId, + userId: message.author.id, + isMentioned, + isOwnedThread, + contentLength: message.content.length, + }); + + // Strip mentions from content + const content = message.content.replace(/<@[!&]?\d+>/g, "").trim(); + if (!content) { + await message.reply("Tag me with a question!").catch(() => {}); + return; + } + + let thread: ThreadChannel; + let parentChannelId: string; + let parentCategoryId: string | null = null; + + try { + if (isInThread) { + thread = message.channel as ThreadChannel; + parentChannelId = thread.parentId ?? ""; + // Get the category from the parent channel + const parentChannel = thread.parent; + parentCategoryId = parentChannel?.parentId ?? null; + } else { + parentChannelId = message.channelId; + parentCategoryId = (message.channel as TextChannel).parentId ?? null; + + if (!isChannelAllowed(parentChannelId, parentCategoryId, env)) { + return; + } + + const threadName = await generateThreadName(content); + thread = await (message.channel as TextChannel).threads.create({ + name: threadName, + startMessage: message, + autoArchiveDuration: 60, + }); + } + + // Check allowed for threads too + if (!isOwnedThread && !isChannelAllowed(parentChannelId, parentCategoryId, env)) { + return; + } + + const threadId = thread.id; + const channelId = parentChannelId; + const guildId = message.guildId ?? ""; + + const trackedBeforeResolve = await sandboxManager.getTrackedSession(threadId); + + // Typing indicator + let typingActive = true; + const sendTyping = () => { + if (typingActive) thread.sendTyping().catch(() => {}); + }; + sendTyping(); + const typingInterval = setInterval(sendTyping, 8000); + + try { + let session = await sandboxManager.resolveSessionForMessage(threadId, channelId, guildId); + let historyPromptCache: { prompt: string; historyCount: number } | null = null; + + const getHistoryPrompt = async (): Promise<{ prompt: string; historyCount: number }> => { + if (!historyPromptCache) { + historyPromptCache = await buildHistoryReplayPrompt(thread, message, content); + } + return historyPromptCache; + }; + + let promptForAgent = content; + if (trackedBeforeResolve && trackedBeforeResolve.sessionId !== session.sessionId) { + const replay = await getHistoryPrompt(); + promptForAgent = replay.prompt; + logger.info({ + event: "discord.context.replayed", + component: "message-handler", + message: "Replayed thread history into replacement session", + threadId, + previousSessionId: trackedBeforeResolve.sessionId, + sessionId: session.sessionId, + historyMessages: replay.historyCount, + }); + } + + let response: string; + try { + response = await sandboxManager.sendMessage(session, promptForAgent); + } catch (err: any) { + if (err?.recoverable && err?.message === "SANDBOX_DEAD") { + logger.warn({ + event: "discord.message.recovering", + component: "message-handler", + message: "Recovering by resolving session again", + threadId, + }); + await thread.send("*Session changed state, recovering...*").catch(() => {}); + const sessionBeforeRecovery = session.sessionId; + session = await sandboxManager.resolveSessionForMessage(threadId, channelId, guildId); + let recoveryPrompt = content; + if (sessionBeforeRecovery !== session.sessionId) { + const replay = await getHistoryPrompt(); + recoveryPrompt = replay.prompt; + logger.info({ + event: "discord.context.replayed", + component: "message-handler", + message: "Replayed thread history after recovery", + threadId, + previousSessionId: sessionBeforeRecovery, + sessionId: session.sessionId, + historyMessages: replay.historyCount, + }); + } + response = await sandboxManager.sendMessage(session, recoveryPrompt); + } else { + throw err; + } + } + + const cleaned = cleanResponse(response); + const chunks = splitForDiscord(cleaned); + + for (const chunk of chunks) { + await thread.send(chunk); + } + } catch (err) { + logger.error({ + event: "discord.message.failed", + component: "message-handler", + message: "Error handling message", + threadId, + error: err, + }); + const errorMsg = err instanceof Error ? err.message : "Unknown error"; + await thread.send(`Something went wrong: ${errorMsg}`).catch(() => {}); + } finally { + typingActive = false; + clearInterval(typingInterval); + } + } catch (err) { + logger.error({ + event: "discord.message.setup_failed", + component: "message-handler", + message: "Error processing message", + channelId: message.channelId, + error: err, + }); + await message.reply("Something went wrong setting up the thread.").catch(() => {}); + } + }; +} diff --git a/packages/discord/src/discord/thread-name.ts b/packages/discord/src/discord/thread-name.ts new file mode 100644 index 0000000000..4acc7c9b4f --- /dev/null +++ b/packages/discord/src/discord/thread-name.ts @@ -0,0 +1,59 @@ +import { getEnv } from "../config"; + +const ZEN_MESSAGES_URL = "https://opencode.ai/zen/v1/messages"; + +/** + * Uses Claude Haiku 4.5 via OpenCode Zen to generate a concise thread name + * from the user's message. Falls back to truncation on error. + */ +export async function generateThreadName(userMessage: string): Promise { + try { + const env = getEnv(); + + const res = await fetch(ZEN_MESSAGES_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": env.OPENCODE_ZEN_API_KEY, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: "claude-haiku-4-5", + max_tokens: 60, + messages: [ + { + role: "user", + content: `Generate a short, descriptive thread title (max 90 chars) for this Discord question. Return ONLY the title, no quotes, no explanation.\n\nQuestion: ${userMessage}`, + }, + ], + }), + }); + + if (!res.ok) { + console.warn(`[thread-name] Zen API returned ${res.status}, falling back to truncation`); + return fallback(userMessage); + } + + const data = await res.json() as { + content?: Array<{ type: string; text?: string }>; + }; + + const title = data.content + ?.filter((c) => c.type === "text") + .map((c) => c.text ?? "") + .join("") + .trim(); + + if (!title || title.length === 0) return fallback(userMessage); + + // Ensure it fits Discord's thread name limit (100 chars) + return title.slice(0, 95) + (title.length > 95 ? "..." : ""); + } catch (err) { + console.warn("[thread-name] Failed to generate name:", err); + return fallback(userMessage); + } +} + +function fallback(message: string): string { + return message.slice(0, 95) + (message.length > 95 ? "..." : ""); +} diff --git a/packages/discord/src/discord/turn-routing.ts b/packages/discord/src/discord/turn-routing.ts new file mode 100644 index 0000000000..42975bed5d --- /dev/null +++ b/packages/discord/src/discord/turn-routing.ts @@ -0,0 +1,127 @@ +const ZEN_MESSAGES_URL = "https://opencode.ai/zen/v1/messages"; + +export type TurnRoutingMode = "off" | "heuristic" | "ai"; + +type TurnRoutingInput = { + mode: TurnRoutingMode; + model: string; + apiKey: string; + content: string; + botUserId: string; + botRoleId: string; + mentionedUserIds: string[]; + mentionedRoleIds: string[]; +}; + +export type TurnRoutingDecision = { + shouldRespond: boolean; + reason: string; +}; + +const QUICK_CHAT_RE = /^(ok|okay|k|kk|thanks|thank you|thx|lol|lmao|haha|nice|cool|yup|yep|nah|nope|got it|sgtm)[!. ]*$/i; + +function heuristicDecision(input: TurnRoutingInput): TurnRoutingDecision | null { + const text = input.content.trim(); + const lower = text.toLowerCase(); + + if (!text) { + return { shouldRespond: false, reason: "empty-message" }; + } + + const mentionsOtherUser = input.mentionedUserIds.some((id) => id !== input.botUserId); + if (mentionsOtherUser) { + return { shouldRespond: false, reason: "mentions-other-user" }; + } + + const mentionsOtherRole = input.mentionedRoleIds.some((id) => id !== input.botRoleId); + if (mentionsOtherRole) { + return { shouldRespond: false, reason: "mentions-other-role" }; + } + + if (text.length <= 40 && QUICK_CHAT_RE.test(text)) { + return { shouldRespond: false, reason: "quick-chat" }; + } + + if (/\b(opencode|bot)\b/i.test(text)) { + return { shouldRespond: true, reason: "bot-keyword" }; + } + + if (text.includes("?") && /\b(you|your|can you|could you|would you|please|help)\b/i.test(text)) { + return { shouldRespond: true, reason: "direct-question" }; + } + + if (text.includes("?") && /\b(how|what|why|where|when|which)\b/i.test(text)) { + return { shouldRespond: true, reason: "general-question" }; + } + + if (lower.startsWith("do this") || lower.startsWith("run ") || lower.startsWith("fix ")) { + return { shouldRespond: true, reason: "instruction" }; + } + + return null; +} + +async function aiDecision(input: TurnRoutingInput): Promise { + const prompt = [ + "You route turns for an engineering Discord bot.", + "Decide if the latest message is directed at the bot assistant or is side conversation.", + "Return EXACTLY one token: RESPOND or SKIP.", + "", + `Message: ${input.content}`, + `MentionsOtherUser: ${input.mentionedUserIds.some((id) => id !== input.botUserId)}`, + `MentionsOtherRole: ${input.mentionedRoleIds.some((id) => id !== input.botRoleId)}`, + ].join("\n"); + + const res = await fetch(ZEN_MESSAGES_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": input.apiKey, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: input.model, + max_tokens: 10, + messages: [{ role: "user", content: prompt }], + }), + }); + + if (!res.ok) { + return { shouldRespond: true, reason: `ai-http-${res.status}` }; + } + + const data = await res.json() as { content?: Array<{ type: string; text?: string }> }; + const output = data.content + ?.filter((c) => c.type === "text") + .map((c) => c.text ?? "") + .join(" ") + .trim() + .toUpperCase(); + + if (output?.includes("SKIP")) { + return { shouldRespond: false, reason: "ai-skip" }; + } + + return { shouldRespond: true, reason: output?.includes("RESPOND") ? "ai-respond" : "ai-default-respond" }; +} + +export async function shouldRespondToOwnedThreadTurn(input: TurnRoutingInput): Promise { + if (input.mode === "off") { + return { shouldRespond: true, reason: "routing-off" }; + } + + const heuristic = heuristicDecision(input); + if (heuristic) { + return heuristic; + } + + if (input.mode === "heuristic") { + return { shouldRespond: true, reason: "heuristic-uncertain-default-respond" }; + } + + try { + return await aiDecision(input); + } catch { + return { shouldRespond: true, reason: "ai-error-default-respond" }; + } +} diff --git a/packages/discord/src/http/health.ts b/packages/discord/src/http/health.ts new file mode 100644 index 0000000000..a0d526ff7e --- /dev/null +++ b/packages/discord/src/http/health.ts @@ -0,0 +1,43 @@ +import type { Client } from "discord.js"; + +type HealthDependencies = { + client: Client; + isCleanupLoopRunning: () => boolean; + getActiveSessionCount: () => Promise; +}; + +export function startHealthServer(host: string, port: number, deps: HealthDependencies): Bun.Server { + const startedAt = Date.now(); + + return Bun.serve({ + hostname: host, + port, + fetch: async (request) => { + const url = new URL(request.url); + + if (url.pathname === "/healthz") { + const activeSessions = await deps.getActiveSessionCount().catch(() => 0); + return Response.json({ + ok: true, + uptimeSec: Math.floor((Date.now() - startedAt) / 1000), + discordReady: deps.client.isReady(), + cleanupLoopRunning: deps.isCleanupLoopRunning(), + activeSessions, + }); + } + + if (url.pathname === "/readyz") { + const ready = deps.client.isReady(); + return Response.json( + { + ok: ready, + discordReady: ready, + }, + { status: ready ? 200 : 503 }, + ); + } + + return new Response("Not Found", { status: 404 }); + }, + }); +} diff --git a/packages/discord/src/index.ts b/packages/discord/src/index.ts new file mode 100644 index 0000000000..ad48fc00a2 --- /dev/null +++ b/packages/discord/src/index.ts @@ -0,0 +1,83 @@ +import { getEnv } from "./config"; +import { createDiscordClient } from "./discord/client"; +import { createMessageHandler } from "./discord/handlers/message-create"; +import { initializeDatabase } from "./db/init"; +import { startHealthServer } from "./http/health"; +import { logger } from "./observability/logger"; +import { SandboxManager } from "./sandbox/manager"; + +async function main() { + const env = getEnv(); + logger.info({ event: "app.starting", component: "index", message: "Starting Discord bot" }); + await initializeDatabase(); + logger.info({ event: "db.ready", component: "index", message: "Database ready" }); + + const client = createDiscordClient(); + const sandboxManager = new SandboxManager(); + const healthServer = startHealthServer(env.HEALTH_HOST, env.HEALTH_PORT, { + client, + isCleanupLoopRunning: () => sandboxManager.isCleanupLoopRunning(), + getActiveSessionCount: () => sandboxManager.getActiveSessionCount(), + }); + + logger.info({ + event: "health.server.started", + component: "index", + message: "Health server started", + host: env.HEALTH_HOST, + port: env.HEALTH_PORT, + }); + + // Register message handler + const messageHandler = createMessageHandler(client, sandboxManager); + client.on("messageCreate", messageHandler); + + // Ready event + client.on("clientReady", () => { + logger.info({ + event: "discord.ready", + component: "index", + message: "Discord client ready", + tag: client.user?.tag, + allowedChannels: env.ALLOWED_CHANNEL_IDS, + }); + sandboxManager.startCleanupLoop(); + }); + + // Login + await client.login(env.DISCORD_TOKEN); + + let shuttingDown = false; + + // Graceful shutdown + const shutdown = async (signal: string) => { + if (shuttingDown) return; + shuttingDown = true; + + logger.info({ + event: "app.shutdown.start", + component: "index", + message: "Shutting down", + signal, + }); + healthServer.stop(); + sandboxManager.stopCleanupLoop(); + await sandboxManager.destroyAll(); + client.destroy(); + logger.info({ event: "app.shutdown.complete", component: "index", message: "Shutdown complete" }); + process.exit(0); + }; + + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGTERM", () => shutdown("SIGTERM")); +} + +main().catch((err) => { + logger.error({ + event: "app.fatal", + component: "index", + message: "Fatal error", + error: err, + }); + process.exit(1); +}); diff --git a/packages/discord/src/observability/logger.ts b/packages/discord/src/observability/logger.ts new file mode 100644 index 0000000000..183483fb0b --- /dev/null +++ b/packages/discord/src/observability/logger.ts @@ -0,0 +1,76 @@ +import { getEnv } from "../config"; + +type LogLevel = "debug" | "info" | "warn" | "error"; + +type LogFields = { + event: string; + message: string; + component?: string; + threadId?: string; + channelId?: string; + guildId?: string; + sandboxId?: string; + sessionId?: string; + durationMs?: number; + [key: string]: unknown; +}; + +const levelOrder: Record = { + debug: 10, + info: 20, + warn: 30, + error: 40, +}; + +function shouldLog(level: LogLevel): boolean { + const env = getEnv(); + return levelOrder[level] >= levelOrder[env.LOG_LEVEL]; +} + +function serializeError(err: unknown) { + if (!(err instanceof Error)) return err; + return { + name: err.name, + message: err.message, + stack: err.stack, + }; +} + +function write(level: LogLevel, fields: LogFields): void { + if (!shouldLog(level)) return; + + const env = getEnv(); + const payload = { + ts: new Date().toISOString(), + level, + ...fields, + }; + + if (env.LOG_PRETTY) { + const line = `[${payload.ts}] ${level.toUpperCase()} ${fields.event} ${fields.message}`; + console.log(line, JSON.stringify(payload)); + return; + } + + console.log(JSON.stringify(payload)); +} + +export const logger = { + debug(fields: LogFields) { + write("debug", fields); + }, + info(fields: LogFields) { + write("info", fields); + }, + warn(fields: LogFields) { + write("warn", fields); + }, + error(fields: LogFields & { error?: unknown }) { + write("error", { + ...fields, + error: fields.error ? serializeError(fields.error) : undefined, + }); + }, +}; + +export type { LogFields, LogLevel }; diff --git a/packages/discord/src/sandbox/image.ts b/packages/discord/src/sandbox/image.ts new file mode 100644 index 0000000000..0d175d50ca --- /dev/null +++ b/packages/discord/src/sandbox/image.ts @@ -0,0 +1,15 @@ +import { Image } from "@daytonaio/sdk"; + +/** + * Custom Daytona sandbox image with git, gh CLI, opencode, and bun. + * Cached by Daytona for 24h — subsequent creates are near-instant. + */ +export function getDiscordBotImage() { + return Image.base("node:22-bookworm-slim") + .runCommands( + "apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*", + "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" > /etc/apt/sources.list.d/github-cli.list && apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/*", + "npm install -g opencode-ai@latest bun", + ) + .workdir("/home/daytona"); +} diff --git a/packages/discord/src/sandbox/manager.ts b/packages/discord/src/sandbox/manager.ts new file mode 100644 index 0000000000..baa276ab02 --- /dev/null +++ b/packages/discord/src/sandbox/manager.ts @@ -0,0 +1,735 @@ +import { Daytona } from "@daytonaio/sdk"; +import { readFileSync } from "node:fs"; +import { getEnv } from "../config"; +import { logger } from "../observability/logger"; +import { getSessionStore } from "../sessions/store"; +import type { SessionInfo, SessionStatus } from "../types"; +import { getDiscordBotImage } from "./image"; +import { createSession, listSessions, sendPrompt, sessionExists, waitForHealthy } from "./opencode-client"; + +/** In-memory timeout handles keyed by threadId */ +const timeouts = new Map>(); + +type ResumeAttemptResult = { + session: SessionInfo | null; + allowRecreate: boolean; +}; + +function timer() { + const start = Date.now(); + return { + elapsedMs: () => Date.now() - start, + }; +} + +function createDaytona() { + return new Daytona({ + apiKey: getEnv().DAYTONA_API_KEY, + _experimental: {}, + }); +} + +function isSandboxMissingError(error: unknown): boolean { + const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); + return message.includes("not found") || message.includes("does not exist") || message.includes("destroyed"); +} + +async function exec( + sandbox: { + process: { + executeCommand: ( + cmd: string, + cwd?: string, + env?: Record, + timeout?: number, + ) => Promise<{ exitCode: number; result: string }>; + }; + }, + label: string, + command: string, + context: Pick, + options?: { cwd?: string; env?: Record }, +): Promise { + const t = timer(); + const result = await sandbox.process.executeCommand(command, options?.cwd, options?.env); + + if (result.exitCode !== 0) { + logger.error({ + event: "sandbox.exec.failed", + component: "sandbox-manager", + message: "Sandbox command failed", + threadId: context.threadId, + sandboxId: context.sandboxId, + label, + exitCode: result.exitCode, + durationMs: t.elapsedMs(), + stdout: result.result.slice(0, 500), + }); + throw new Error(`${label} failed (exit ${result.exitCode})`); + } + + logger.debug({ + event: "sandbox.exec.ok", + component: "sandbox-manager", + message: "Sandbox command completed", + threadId: context.threadId, + sandboxId: context.sandboxId, + label, + durationMs: t.elapsedMs(), + }); + + return result.result.trim(); +} + +export class SandboxManager { + private cleanupInterval: ReturnType | null = null; + private readonly store = getSessionStore(); + private readonly threadLocks = new Map>(); + + async getActiveSessionCount(): Promise { + return (await this.store.listActive()).length; + } + + isCleanupLoopRunning(): boolean { + return this.cleanupInterval !== null; + } + + async hasTrackedThread(threadId: string): Promise { + return this.store.hasTrackedThread(threadId); + } + + async getTrackedSession(threadId: string): Promise { + return this.store.getByThread(threadId); + } + + async getSession(threadId: string): Promise { + return this.store.getActive(threadId); + } + + async resolveSessionForMessage(threadId: string, channelId: string, guildId: string): Promise { + return this.withThreadLock(threadId, async () => { + const existing = await this.store.getByThread(threadId); + const env = getEnv(); + + if (!existing) { + return this.createSessionUnlocked(threadId, channelId, guildId); + } + + let candidate = existing; + + if (candidate.status === "active") { + const healthy = await this.ensureSessionHealthy(candidate, 15_000); + if (healthy) return candidate; + candidate = (await this.store.getByThread(threadId)) ?? { ...candidate, status: "error" }; + } + + if (env.SANDBOX_REUSE_POLICY === "resume_preferred") { + const resumed = await this.tryResumeSession(candidate); + if (resumed.session) return resumed.session; + + if (!resumed.allowRecreate) { + throw new Error("Unable to reattach to existing sandbox session. Try again shortly."); + } + } + + return this.createSessionUnlocked(threadId, channelId, guildId); + }); + } + + async createSession(threadId: string, channelId: string, guildId: string): Promise { + return this.withThreadLock(threadId, async () => this.createSessionUnlocked(threadId, channelId, guildId)); + } + + private async createSessionUnlocked(threadId: string, channelId: string, guildId: string): Promise { + const env = getEnv(); + const totalTimer = timer(); + + await this.store.updateStatus(threadId, "creating").catch(() => {}); + + const daytona = createDaytona(); + const image = getDiscordBotImage(); + const sandbox = await daytona.create( + { + image, + labels: { + app: "opencord", + threadId, + guildId, + }, + autoStopInterval: 0, + autoArchiveInterval: 0, + }, + { timeout: env.SANDBOX_CREATION_TIMEOUT }, + ); + + const sandboxId = sandbox.id; + logger.info({ + event: "sandbox.create.started", + component: "sandbox-manager", + message: "Created sandbox", + threadId, + channelId, + guildId, + sandboxId, + }); + + try { + const context = { threadId, sandboxId }; + const home = await exec(sandbox, "discover-home", "echo $HOME", context); + + await exec( + sandbox, + "clone-opencode", + `git clone --depth=1 https://github.com/anomalyco/opencode.git ${home}/opencode`, + context, + ); + + const authJson = JSON.stringify({ + opencode: { type: "api", key: env.OPENCODE_ZEN_API_KEY }, + }); + + await exec( + sandbox, + "write-auth", + `mkdir -p ${home}/.local/share/opencode && cat > ${home}/.local/share/opencode/auth.json << 'AUTHEOF'\n${authJson}\nAUTHEOF`, + context, + ); + + const agentPromptPath = new URL("../agent-prompt.md", import.meta.url); + const agentPrompt = readFileSync(agentPromptPath, "utf-8"); + + const opencodeConfig = JSON.stringify({ + model: env.OPENCODE_MODEL, + share: "disabled", + permission: "allow", + agent: { + build: { + mode: "primary", + prompt: agentPrompt, + }, + }, + }); + + const configB64 = Buffer.from(opencodeConfig).toString("base64"); + await exec( + sandbox, + "write-config", + `echo "${configB64}" | base64 -d > ${home}/opencode/opencode.json`, + context, + ); + + const opencodeEnv = this.buildRuntimeEnv(); + const githubToken = opencodeEnv.GITHUB_TOKEN ?? ""; + + logger.info({ + event: "sandbox.github.auth", + component: "sandbox-manager", + message: githubToken.length > 0 + ? "Configured authenticated gh CLI in sandbox runtime" + : "Running sandbox gh CLI unauthenticated (no GITHUB_TOKEN provided)", + threadId, + sandboxId, + authenticated: githubToken.length > 0, + }); + + await exec( + sandbox, + "start-opencode", + "setsid opencode serve --port 4096 --hostname 0.0.0.0 > /tmp/opencode.log 2>&1 &", + context, + { + cwd: `${home}/opencode`, + env: opencodeEnv, + }, + ); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const preview = await sandbox.getPreviewLink(4096); + const previewUrl = preview.url.replace(/\/$/, ""); + const previewToken = preview.token ?? null; + + const healthy = await waitForHealthy({ previewUrl, previewToken }, 120_000); + if (!healthy) { + const startupLog = await exec(sandbox, "read-opencode-log", "cat /tmp/opencode.log 2>/dev/null | tail -100", context); + throw new Error(`OpenCode server did not become healthy: ${startupLog.slice(0, 400)}`); + } + + const sessionId = await createSession({ previewUrl, previewToken }, `Discord thread ${threadId}`); + + const session: SessionInfo = { + threadId, + channelId, + guildId, + sandboxId, + sessionId, + previewUrl, + previewToken, + status: "active", + }; + + await this.store.upsert(session); + await this.store.markHealthOk(threadId); + this.resetTimeout(threadId); + + logger.info({ + event: "sandbox.create.ready", + component: "sandbox-manager", + message: "Session is ready", + threadId, + channelId, + guildId, + sandboxId, + sessionId, + durationMs: totalTimer.elapsedMs(), + }); + + return session; + } catch (error) { + logger.error({ + event: "sandbox.create.failed", + component: "sandbox-manager", + message: "Failed to create session", + threadId, + channelId, + guildId, + sandboxId, + durationMs: totalTimer.elapsedMs(), + error, + }); + + await this.store.updateStatus(threadId, "error", error instanceof Error ? error.message : String(error)).catch(() => {}); + await daytona.delete(sandbox).catch(() => {}); + throw error; + } + } + + async sendMessage(session: SessionInfo, text: string): Promise { + return this.withThreadLock(session.threadId, async () => { + const t = timer(); + await this.store.markActivity(session.threadId); + this.resetTimeout(session.threadId); + + try { + const response = await sendPrompt( + { previewUrl: session.previewUrl, previewToken: session.previewToken }, + session.sessionId, + text, + ); + + logger.info({ + event: "session.message.ok", + component: "sandbox-manager", + message: "Message processed", + threadId: session.threadId, + sandboxId: session.sandboxId, + sessionId: session.sessionId, + durationMs: t.elapsedMs(), + responseChars: response.length, + }); + + return response; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const sessionMissing = message.includes("Failed to send prompt (404"); + const recoverable = + message.includes("no IP address found") || + message.includes("Is the Sandbox started") || + message.includes("sandbox not found") || + message.includes("Failed to send prompt (5") || + sessionMissing; + + if (recoverable) { + await this.store.incrementResumeFailure(session.threadId, message); + if (sessionMissing) { + await this.store.updateStatus(session.threadId, "error", "opencode-session-missing").catch(() => {}); + } else { + await this.pauseSessionUnlocked(session.threadId, "recoverable send failure").catch(() => {}); + } + const recoveryError = new Error("SANDBOX_DEAD"); + (recoveryError as any).recoverable = true; + throw recoveryError; + } + + throw error; + } + }); + } + + async pauseSession(threadId: string, reason = "manual"): Promise { + await this.withThreadLock(threadId, async () => { + await this.pauseSessionUnlocked(threadId, reason); + }); + } + + private async pauseSessionUnlocked(threadId: string, reason: string): Promise { + const session = await this.store.getByThread(threadId); + if (!session) return; + if (session.status === "paused") return; + + await this.store.updateStatus(threadId, "pausing", reason); + + try { + const daytona = createDaytona(); + const sandbox = await daytona.get(session.sandboxId); + await daytona.stop(sandbox); + await this.store.updateStatus(threadId, "paused", null); + this.clearTimeout(threadId); + + logger.info({ + event: "sandbox.paused", + component: "sandbox-manager", + message: "Paused sandbox", + threadId, + sandboxId: session.sandboxId, + reason, + }); + } catch (error) { + await this.store.updateStatus(threadId, "destroyed", error instanceof Error ? error.message : String(error)); + logger.warn({ + event: "sandbox.pause.missing", + component: "sandbox-manager", + message: "Sandbox unavailable while pausing; marked destroyed", + threadId, + sandboxId: session.sandboxId, + }); + } + } + + private async tryResumeSession(session: SessionInfo): Promise { + if (!["paused", "destroyed", "error", "pausing", "resuming"].includes(session.status)) { + return { session: null, allowRecreate: true }; + } + + await this.store.updateStatus(session.threadId, "resuming"); + + const daytona = createDaytona(); + let sandbox: Awaited>; + + try { + sandbox = await daytona.get(session.sandboxId); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await this.store.incrementResumeFailure(session.threadId, errorMessage).catch(() => {}); + await this.store.updateStatus(session.threadId, "destroyed", errorMessage).catch(() => {}); + + logger.warn({ + event: "sandbox.resume.sandbox_missing", + component: "sandbox-manager", + message: "Sandbox missing during resume; safe to recreate", + threadId: session.threadId, + sandboxId: session.sandboxId, + errorMessage, + }); + + return { session: null, allowRecreate: true }; + } + + try { + await daytona.start(sandbox, getEnv().SANDBOX_CREATION_TIMEOUT); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await this.store.incrementResumeFailure(session.threadId, errorMessage).catch(() => {}); + await this.store.updateStatus(session.threadId, "error", errorMessage).catch(() => {}); + + const allowRecreate = isSandboxMissingError(error); + logger.warn({ + event: "sandbox.resume.start_failed", + component: "sandbox-manager", + message: allowRecreate + ? "Sandbox no longer exists while starting; safe to recreate" + : "Sandbox start failed; refusing automatic recreate to avoid context loss", + threadId: session.threadId, + sandboxId: session.sandboxId, + errorMessage, + allowRecreate, + }); + + return { session: null, allowRecreate }; + } + + try { + const preview = await sandbox.getPreviewLink(4096); + const previewUrl = preview.url.replace(/\/$/, ""); + const previewToken = preview.token ?? null; + + const context = { threadId: session.threadId, sandboxId: session.sandboxId }; + + logger.info({ + event: "sandbox.resume.restarting_opencode", + component: "sandbox-manager", + message: "Restarting opencode serve after sandbox start", + threadId: session.threadId, + sandboxId: session.sandboxId, + }); + + await exec( + sandbox, + "restart-opencode-serve", + "pkill -f 'opencode serve --port 4096' >/dev/null 2>&1 || true; for d in \"$HOME/opencode\" \"/home/daytona/opencode\" \"/root/opencode\"; do if [ -d \"$d\" ]; then cd \"$d\" && setsid opencode serve --port 4096 --hostname 0.0.0.0 > /tmp/opencode.log 2>&1 & exit 0; fi; done; exit 1", + context, + { env: this.buildRuntimeEnv() }, + ); + + const healthy = await waitForHealthy( + { previewUrl, previewToken }, + getEnv().RESUME_HEALTH_TIMEOUT_MS, + ); + + if (!healthy) { + const startupLog = await exec( + sandbox, + "read-opencode-log-after-resume", + "cat /tmp/opencode.log 2>/dev/null | tail -120", + context, + ).catch(() => "(unable to read opencode log)"); + + const errorMessage = `OpenCode health check failed after resume. Log: ${startupLog.slice(0, 500)}`; + await this.store.incrementResumeFailure(session.threadId, errorMessage).catch(() => {}); + await this.store.updateStatus(session.threadId, "error", errorMessage).catch(() => {}); + + logger.error({ + event: "sandbox.resume.health_failed", + component: "sandbox-manager", + message: "OpenCode did not become healthy after restart; refusing recreate", + threadId: session.threadId, + sandboxId: session.sandboxId, + errorMessage, + }); + + return { session: null, allowRecreate: false }; + } + + let sessionId = session.sessionId; + const existingSession = await sessionExists({ previewUrl, previewToken }, sessionId); + if (!existingSession) { + const expectedTitle = `Discord thread ${session.threadId}`; + const sessions = await listSessions({ previewUrl, previewToken }, 50).catch(() => []); + + const replacement = sessions + .filter((candidate) => candidate.title === expectedTitle) + .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0]; + + if (replacement) { + sessionId = replacement.id; + logger.info({ + event: "sandbox.resume.session_reused_by_title", + component: "sandbox-manager", + message: "Reattached to existing session by title", + threadId: session.threadId, + sandboxId: session.sandboxId, + previousSessionId: session.sessionId, + sessionId, + }); + } else { + logger.warn({ + event: "sandbox.resume.session_missing", + component: "sandbox-manager", + message: "OpenCode session missing after resume; creating replacement session", + threadId: session.threadId, + sandboxId: session.sandboxId, + sessionId, + }); + + sessionId = await createSession({ previewUrl, previewToken }, expectedTitle); + } + } + + const resumed: SessionInfo = { + ...session, + sessionId, + previewUrl, + previewToken, + status: "active", + }; + + await this.store.upsert(resumed); + await this.store.markHealthOk(session.threadId); + this.resetTimeout(session.threadId); + + logger.info({ + event: "sandbox.resumed", + component: "sandbox-manager", + message: "Resumed existing sandbox", + threadId: session.threadId, + sandboxId: session.sandboxId, + previousSessionId: session.sessionId, + sessionId, + sessionReattached: sessionId === session.sessionId, + }); + + return { session: resumed, allowRecreate: false }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + await this.store.incrementResumeFailure(session.threadId, errorMessage).catch(() => {}); + await this.store.updateStatus(session.threadId, "error", errorMessage).catch(() => {}); + + logger.warn({ + event: "sandbox.resume.failed", + component: "sandbox-manager", + message: "Resume failed after sandbox start; refusing automatic recreate", + threadId: session.threadId, + sandboxId: session.sandboxId, + errorMessage, + }); + + return { session: null, allowRecreate: false }; + } + } + + async destroySession(threadId: string): Promise { + await this.withThreadLock(threadId, async () => { + const session = await this.store.getByThread(threadId); + if (!session) return; + + await this.store.updateStatus(threadId, "destroying"); + + try { + const daytona = createDaytona(); + const sandbox = await daytona.get(session.sandboxId); + await daytona.delete(sandbox); + } catch { + // no-op + } + + await this.store.updateStatus(threadId, "destroyed"); + this.clearTimeout(threadId); + }); + } + + resetTimeout(threadId: string): void { + this.clearTimeout(threadId); + const timeoutMs = getEnv().SANDBOX_TIMEOUT_MINUTES * 60 * 1000; + + const handle = setTimeout(async () => { + timeouts.delete(threadId); + await this.pauseSession(threadId, "inactivity-timeout").catch((error) => { + logger.error({ + event: "sandbox.pause.timeout.failed", + component: "sandbox-manager", + message: "Failed to pause sandbox on inactivity timeout", + threadId, + error, + }); + }); + }, timeoutMs); + + timeouts.set(threadId, handle); + } + + private clearTimeout(threadId: string): void { + const existing = timeouts.get(threadId); + if (!existing) return; + clearTimeout(existing); + timeouts.delete(threadId); + } + + startCleanupLoop(): void { + const intervalMs = 5 * 60 * 1000; + + this.cleanupInterval = setInterval(async () => { + try { + const env = getEnv(); + const staleActive = await this.store.listStaleActive(env.SANDBOX_TIMEOUT_MINUTES + 5); + + for (const session of staleActive) { + await this.pauseSession(session.threadId, "cleanup-stale-active"); + } + + const expiredPaused = await this.store.listExpiredPaused(env.PAUSED_TTL_MINUTES); + for (const session of expiredPaused) { + await this.destroySession(session.threadId); + } + } catch (error) { + logger.error({ + event: "cleanup.loop.failed", + component: "sandbox-manager", + message: "Cleanup loop failed", + error, + }); + } + }, intervalMs); + + logger.info({ + event: "cleanup.loop.started", + component: "sandbox-manager", + message: "Started cleanup loop", + intervalMs, + timeoutMinutes: getEnv().SANDBOX_TIMEOUT_MINUTES, + pausedTtlMinutes: getEnv().PAUSED_TTL_MINUTES, + }); + } + + stopCleanupLoop(): void { + if (!this.cleanupInterval) return; + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + + async destroyAll(): Promise { + const active = await this.store.listActive(); + await Promise.allSettled(active.map((session) => this.pauseSession(session.threadId, "shutdown"))); + } + + private buildRuntimeEnv(): Record { + const env = getEnv(); + const runtimeEnv: Record = {}; + const githubToken = env.GITHUB_TOKEN.trim(); + + if (githubToken.length > 0) { + runtimeEnv.GH_TOKEN = githubToken; + runtimeEnv.GITHUB_TOKEN = githubToken; + } + + return runtimeEnv; + } + + private async ensureSessionHealthy(session: SessionInfo, maxWaitMs: number): Promise { + const healthy = await waitForHealthy( + { previewUrl: session.previewUrl, previewToken: session.previewToken }, + maxWaitMs, + ); + + if (!healthy) { + await this.store.incrementResumeFailure(session.threadId, "active-session-healthcheck-failed").catch(() => {}); + await this.store.updateStatus(session.threadId, "error", "active-session-healthcheck-failed").catch(() => {}); + return false; + } + + const attached = await sessionExists( + { previewUrl: session.previewUrl, previewToken: session.previewToken }, + session.sessionId, + ).catch(() => false); + + if (!attached) { + await this.store.incrementResumeFailure(session.threadId, "active-session-missing").catch(() => {}); + await this.store.updateStatus(session.threadId, "error", "active-session-missing").catch(() => {}); + return false; + } + + await this.store.markHealthOk(session.threadId).catch(() => {}); + return true; + } + + private async withThreadLock(threadId: string, fn: () => Promise): Promise { + const previous = this.threadLocks.get(threadId) ?? Promise.resolve(); + let release!: () => void; + const current = new Promise((resolve) => { + release = resolve; + }); + + this.threadLocks.set(threadId, previous.then(() => current)); + await previous; + + try { + return await fn(); + } finally { + release(); + if (this.threadLocks.get(threadId) === current) { + this.threadLocks.delete(threadId); + } + } + } +} + +export type { SessionStatus }; diff --git a/packages/discord/src/sandbox/opencode-client.ts b/packages/discord/src/sandbox/opencode-client.ts new file mode 100644 index 0000000000..32c6fc2d8b --- /dev/null +++ b/packages/discord/src/sandbox/opencode-client.ts @@ -0,0 +1,198 @@ +import { Effect, Schedule } from "effect"; +import { logger } from "../observability/logger"; + +type PreviewAccess = { + previewUrl: string; + previewToken?: string | null; +}; + +export type OpenCodeSessionSummary = { + id: string; + title: string; + updatedAt?: number; +}; + +/** + * Parse a Daytona preview URL into base URL and token. + * Preview URLs look like: https://4096-xxx.proxy.daytona.works?tkn=abc123 + */ +function parsePreview(input: string | PreviewAccess): { base: string; token: string | null } { + const previewUrl = typeof input === "string" ? input : input.previewUrl; + const url = new URL(previewUrl); + const token = typeof input === "string" + ? url.searchParams.get("tkn") + : (input.previewToken ?? url.searchParams.get("tkn")); + url.searchParams.delete("tkn"); + return { base: url.toString().replace(/\/$/, ""), token }; +} + +/** + * Fetch wrapper that properly handles Daytona preview URL token auth. + * Sends token as x-daytona-preview-token header. + */ +async function previewFetch(preview: string | PreviewAccess, path: string, init?: RequestInit): Promise { + const { base, token } = parsePreview(preview); + const url = `${base}${path}`; + const headers = new Headers(init?.headers); + if (token) { + headers.set("x-daytona-preview-token", token); + } + return fetch(url, { ...init, headers }); +} + +/** + * Waits for the OpenCode server inside a sandbox to become healthy. + * Polls GET /global/health every 2s up to maxWaitMs. + */ +export async function waitForHealthy(preview: string | PreviewAccess, maxWaitMs = 120_000): Promise { + const start = Date.now(); + let lastStatus = ""; + + const poll = Effect.tryPromise(async () => { + const res = await previewFetch(preview, "/global/health"); + lastStatus = `${res.status}`; + + if (res.ok) { + const body = await res.json() as { healthy?: boolean }; + if (body.healthy) return true; + lastStatus = `200 but healthy=${body.healthy}`; + throw new Error(lastStatus); + } + + const body = await res.text().catch(() => ""); + lastStatus = `${res.status}: ${body.slice(0, 150)}`; + throw new Error(lastStatus); + }).pipe( + Effect.tapError(() => + Effect.sync(() => { + const elapsed = ((Date.now() - start) / 1000).toFixed(0); + logger.warn({ + event: "opencode.health.poll", + component: "opencode-client", + message: "Health check poll failed", + elapsedSec: Number(elapsed), + lastStatus, + }); + }), + ), + ); + + const maxAttempts = Math.max(1, Math.ceil(maxWaitMs / 2000)); + + return Effect.runPromise( + poll.pipe( + Effect.retry( + Schedule.intersect( + Schedule.spaced("2 seconds"), + Schedule.recurs(maxAttempts - 1), + ), + ), + Effect.as(true), + Effect.catchAll(() => + Effect.sync(() => { + logger.error({ + event: "opencode.health.failed", + component: "opencode-client", + message: "Health check failed", + maxWaitMs, + lastStatus, + }); + return false; + }), + ), + ), + ); +} + +/** + * Creates a new OpenCode session and returns the session ID. + */ +export async function createSession(preview: string | PreviewAccess, title: string): Promise { + const res = await previewFetch(preview, "/session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title }), + }); + + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`Failed to create session (${res.status}): ${body}`); + } + + const session = await res.json() as { id: string }; + return session.id; +} + +export async function sessionExists(preview: string | PreviewAccess, sessionId: string): Promise { + const res = await previewFetch(preview, `/session/${sessionId}`, { + method: "GET", + }); + + if (res.ok) return true; + if (res.status === 404) return false; + + const body = await res.text().catch(() => ""); + throw new Error(`Failed to check session (${res.status}): ${body}`); +} + +export async function listSessions(preview: string | PreviewAccess, limit = 50): Promise { + const query = limit > 0 ? `?limit=${limit}` : ""; + const res = await previewFetch(preview, `/session${query}`, { + method: "GET", + }); + + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`Failed to list sessions (${res.status}): ${body}`); + } + + const sessions = await res.json() as Array<{ + id?: string; + title?: string; + time?: { updated?: number }; + }>; + + return sessions + .filter((session) => typeof session.id === "string") + .map((session) => ({ + id: session.id as string, + title: session.title ?? "", + updatedAt: session.time?.updated, + })); +} + +/** + * Sends a prompt to an existing session and returns the text response. + * This call blocks until the agent finishes processing. + */ +export async function sendPrompt(preview: string | PreviewAccess, sessionId: string, text: string): Promise { + const res = await previewFetch(preview, `/session/${sessionId}/message`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + parts: [{ type: "text", text }], + }), + }); + + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`Failed to send prompt (${res.status}): ${body}`); + } + + const result = await res.json() as { parts?: Array<{ type: string; text?: string; content?: string }> }; + const parts = result.parts ?? []; + + const textContent = parts + .filter((p) => p.type === "text") + .map((p) => p.text || p.content || "") + .filter(Boolean); + + return textContent.join("\n\n") || "(No response from agent)"; +} + +/** + * Aborts a running session. + */ +export async function abortSession(preview: string | PreviewAccess, sessionId: string): Promise { + await previewFetch(preview, `/session/${sessionId}/abort`, { method: "POST" }).catch(() => {}); +} diff --git a/packages/discord/src/sessions/store.ts b/packages/discord/src/sessions/store.ts new file mode 100644 index 0000000000..2cea38bb20 --- /dev/null +++ b/packages/discord/src/sessions/store.ts @@ -0,0 +1,266 @@ +import { getSql } from "../db/client"; +import type { SessionInfo, SessionStatus } from "../types"; + +type SessionRow = { + thread_id: string; + channel_id: string; + guild_id: string; + sandbox_id: string; + session_id: string; + preview_url: string; + preview_token: string | null; + status: SessionStatus; + last_error: string | null; + resume_fail_count: number; +}; + +export interface SessionStore { + upsert(session: SessionInfo): Promise; + getByThread(threadId: string): Promise; + hasTrackedThread(threadId: string): Promise; + getActive(threadId: string): Promise; + markActivity(threadId: string): Promise; + markHealthOk(threadId: string): Promise; + updateStatus(threadId: string, status: SessionStatus, lastError?: string | null): Promise; + incrementResumeFailure(threadId: string, lastError: string): Promise; + listActive(): Promise; + listStaleActive(cutoffMinutes: number): Promise; + listExpiredPaused(pausedTtlMinutes: number): Promise; +} + +class NeonSessionStore implements SessionStore { + private readonly sql = getSql(); + + async upsert(session: SessionInfo): Promise { + await this.sql` + INSERT INTO discord_sessions ( + thread_id, + channel_id, + guild_id, + sandbox_id, + session_id, + preview_url, + preview_token, + status, + last_error, + last_activity, + resumed_at, + created_at, + updated_at + ) VALUES ( + ${session.threadId}, + ${session.channelId}, + ${session.guildId}, + ${session.sandboxId}, + ${session.sessionId}, + ${session.previewUrl}, + ${session.previewToken}, + ${session.status}, + ${session.lastError ?? null}, + NOW(), + CASE WHEN ${session.status} = 'active' THEN NOW() ELSE NULL END, + NOW(), + NOW() + ) + ON CONFLICT (thread_id) + DO UPDATE SET + channel_id = EXCLUDED.channel_id, + guild_id = EXCLUDED.guild_id, + sandbox_id = EXCLUDED.sandbox_id, + session_id = EXCLUDED.session_id, + preview_url = EXCLUDED.preview_url, + preview_token = EXCLUDED.preview_token, + status = EXCLUDED.status, + last_error = EXCLUDED.last_error, + last_activity = NOW(), + resumed_at = CASE WHEN EXCLUDED.status = 'active' THEN NOW() ELSE discord_sessions.resumed_at END, + updated_at = NOW() + `; + } + + async getByThread(threadId: string): Promise { + const rows = await this.sql` + SELECT + thread_id, + channel_id, + guild_id, + sandbox_id, + session_id, + preview_url, + preview_token, + status, + last_error, + resume_fail_count + FROM discord_sessions + WHERE thread_id = ${threadId} + LIMIT 1 + ` as SessionRow[]; + + if (rows.length === 0) return null; + return toSessionInfo(rows[0]); + } + + async hasTrackedThread(threadId: string): Promise { + const rows = await this.sql` + SELECT thread_id + FROM discord_sessions + WHERE thread_id = ${threadId} + LIMIT 1 + ` as Array<{ thread_id: string }>; + + return rows.length > 0; + } + + async getActive(threadId: string): Promise { + const rows = await this.sql` + SELECT + thread_id, + channel_id, + guild_id, + sandbox_id, + session_id, + preview_url, + preview_token, + status, + last_error, + resume_fail_count + FROM discord_sessions + WHERE thread_id = ${threadId} + AND status = 'active' + LIMIT 1 + ` as SessionRow[]; + + if (rows.length === 0) return null; + return toSessionInfo(rows[0]); + } + + async markActivity(threadId: string): Promise { + await this.sql` + UPDATE discord_sessions + SET last_activity = NOW(), updated_at = NOW() + WHERE thread_id = ${threadId} + `; + } + + async markHealthOk(threadId: string): Promise { + await this.sql` + UPDATE discord_sessions + SET last_health_ok_at = NOW(), updated_at = NOW() + WHERE thread_id = ${threadId} + `; + } + + async updateStatus(threadId: string, status: SessionStatus, lastError?: string | null): Promise { + await this.sql` + UPDATE discord_sessions + SET + status = ${status}, + last_error = ${lastError ?? null}, + pause_requested_at = CASE WHEN ${status} = 'pausing' THEN NOW() ELSE pause_requested_at END, + paused_at = CASE WHEN ${status} = 'paused' THEN NOW() ELSE paused_at END, + resume_attempted_at = CASE WHEN ${status} = 'resuming' THEN NOW() ELSE resume_attempted_at END, + resumed_at = CASE WHEN ${status} = 'active' THEN NOW() ELSE resumed_at END, + destroyed_at = CASE WHEN ${status} = 'destroyed' THEN NOW() ELSE destroyed_at END, + updated_at = NOW() + WHERE thread_id = ${threadId} + `; + } + + async incrementResumeFailure(threadId: string, lastError: string): Promise { + await this.sql` + UPDATE discord_sessions + SET + resume_fail_count = resume_fail_count + 1, + last_error = ${lastError}, + updated_at = NOW() + WHERE thread_id = ${threadId} + `; + } + + async listActive(): Promise { + const rows = await this.sql` + SELECT + thread_id, + channel_id, + guild_id, + sandbox_id, + session_id, + preview_url, + preview_token, + status, + last_error, + resume_fail_count + FROM discord_sessions + WHERE status = 'active' + ORDER BY last_activity DESC + ` as SessionRow[]; + + return rows.map(toSessionInfo); + } + + async listStaleActive(cutoffMinutes: number): Promise { + const rows = await this.sql` + SELECT + thread_id, + channel_id, + guild_id, + sandbox_id, + session_id, + preview_url, + preview_token, + status, + last_error, + resume_fail_count + FROM discord_sessions + WHERE status = 'active' + AND last_activity < NOW() - (${cutoffMinutes} || ' minutes')::interval + ORDER BY last_activity ASC + ` as SessionRow[]; + + return rows.map(toSessionInfo); + } + + async listExpiredPaused(pausedTtlMinutes: number): Promise { + const rows = await this.sql` + SELECT + thread_id, + channel_id, + guild_id, + sandbox_id, + session_id, + preview_url, + preview_token, + status, + last_error, + resume_fail_count + FROM discord_sessions + WHERE status = 'paused' + AND paused_at IS NOT NULL + AND paused_at < NOW() - (${pausedTtlMinutes} || ' minutes')::interval + ORDER BY paused_at ASC + ` as SessionRow[]; + + return rows.map(toSessionInfo); + } +} + +function toSessionInfo(row: SessionRow): SessionInfo { + return { + threadId: row.thread_id, + channelId: row.channel_id, + guildId: row.guild_id, + sandboxId: row.sandbox_id, + sessionId: row.session_id, + previewUrl: row.preview_url, + previewToken: row.preview_token, + status: row.status, + lastError: row.last_error, + resumeFailCount: row.resume_fail_count, + }; +} + +const sessionStore: SessionStore = new NeonSessionStore(); + +export function getSessionStore(): SessionStore { + return sessionStore; +} diff --git a/packages/discord/src/types.ts b/packages/discord/src/types.ts new file mode 100644 index 0000000000..1bfd3cdf55 --- /dev/null +++ b/packages/discord/src/types.ts @@ -0,0 +1,22 @@ +export type SessionStatus = + | "creating" + | "active" + | "pausing" + | "paused" + | "resuming" + | "destroying" + | "destroyed" + | "error"; + +export interface SessionInfo { + threadId: string; + channelId: string; + guildId: string; + sandboxId: string; + sessionId: string; + previewUrl: string; + previewToken: string | null; + status: SessionStatus; + lastError?: string | null; + resumeFailCount?: number; +} diff --git a/packages/discord/tsconfig.json b/packages/discord/tsconfig.json new file mode 100644 index 0000000000..7ca1581be4 --- /dev/null +++ b/packages/discord/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "sourceMap": true, + "outDir": "./dist", + "types": ["bun-types", "node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +}