diff --git a/.agents/skills/chat-sdk/SKILL.md b/.agents/skills/chat-sdk/SKILL.md new file mode 100644 index 00000000..ba8fa14e --- /dev/null +++ b/.agents/skills/chat-sdk/SKILL.md @@ -0,0 +1,187 @@ +--- +name: chat-sdk +description: Build multi-platform chat bots with Chat SDK (`chat` npm package). Use when developers want to build a Slack, Teams, Google Chat, Discord, Telegram, GitHub, Linear, or WhatsApp bot, handle mentions, direct messages, subscribed threads, reactions, slash commands, cards, modals, files, or AI streaming, set up webhook routes or multi-adapter bots, send rich cards or streamed AI responses to chat platforms, or build a custom adapter or state adapter. +--- + +# Chat SDK + +Unified TypeScript SDK for building chat bots across Slack, Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp. Write bot logic once, deploy everywhere. + +## Start with published sources + +When Chat SDK is installed in a user project, inspect the published files that ship in `node_modules`: + +``` +node_modules/chat/docs/ # bundled docs +node_modules/chat/dist/index.d.ts # core API types +node_modules/chat/dist/jsx-runtime.d.ts # JSX runtime types +node_modules/chat/docs/contributing/ # adapter-authoring docs +node_modules/chat/resources/guides/ # framework/platform guides (markdown) +node_modules/chat/resources/templates.json # starter templates (title, description, href) +``` + +If one of the paths below does not exist, that package is not installed in the project yet. + +Read these before writing code: +- `node_modules/chat/docs/getting-started.mdx` — install and setup +- `node_modules/chat/docs/usage.mdx` — `Chat` config and lifecycle +- `node_modules/chat/docs/handling-events.mdx` — event routing and handlers +- `node_modules/chat/docs/threads-messages-channels.mdx` — thread/channel/message model +- `node_modules/chat/docs/posting-messages.mdx` — post, edit, delete, schedule +- `node_modules/chat/docs/streaming.mdx` — AI SDK integration and streaming semantics +- `node_modules/chat/docs/cards.mdx` — JSX cards +- `node_modules/chat/docs/actions.mdx` — button/select interactions +- `node_modules/chat/docs/modals.mdx` — modal submit/close flows +- `node_modules/chat/docs/slash-commands.mdx` — slash command routing +- `node_modules/chat/docs/direct-messages.mdx` — DM behavior and `openDM()` +- `node_modules/chat/docs/files.mdx` — attachments/uploads +- `node_modules/chat/docs/state.mdx` — persistence, locking, dedupe +- `node_modules/chat/docs/adapters.mdx` — cross-platform feature matrix +- `node_modules/chat/docs/api/chat.mdx` — exact `Chat` API +- `node_modules/chat/docs/api/thread.mdx` — exact `Thread` API +- `node_modules/chat/docs/api/message.mdx` — exact `Message` API +- `node_modules/chat/docs/api/modals.mdx` — modal element and event details + +For the specific adapter or state package you are using, inspect that installed package's `dist/index.d.ts` export surface in `node_modules`. + +## Available resources + + + +### Guides + +- `node_modules/chat/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md` — Build a Slack AI agent using Chat SDK, AI SDK's ToolLoopAgent, and Vercel AI Gateway. Covers project setup, tool definitions, streaming responses, deployment to Vercel, and scaling tool selection with toolpick. +- `node_modules/chat/resources/guides/run-and-track-deploys-from-slack.md` — Build a Slack deploy bot with Chat SDK and Vercel Workflow. Dispatch GitHub Actions from a slash command, gate production behind approval, poll for completion, and notify Linear and GitHub when the run finishes. +- `node_modules/chat/resources/guides/triage-form-submissions-with-chat-sdk.md` — Build a Slack bot that triages form submissions with interactive cards. Forward, edit, or mark as spam without leaving Slack. Built with Chat SDK, Hono, and Resend. +- `node_modules/chat/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md` — This guide walks through building a Slack bot with Next.js, covering project setup, Slack app configuration, event handling, interactive features, and deployment. +- `node_modules/chat/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md` — This guide walks through building a Discord support bot with Nuxt, covering project setup, Discord app configuration, Gateway forwarding, AI-powered responses, and deployment. +- `node_modules/chat/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md` — This guide walks through building a GitHub bot that reviews pull requests on demand. When a user @mentions the bot on a PR, Chat SDK picks up the mention, spins up a Vercel Sandbox with the repo cloned, and uses AI SDK to analyze the diff. + +### Templates + +Listed in `node_modules/chat/resources/templates.json`: + +- **Chat SDK Liveblocks Bot** — Build a bot that you can engage with inside Liveblocks. (https://vercel.com/templates/next.js/chat-sdk-liveblocks-bot) +- **Knowledge Agent** — Open source file-system and knowledge based agent template. Build AI agents that stay up to date with your knowledge base. (https://vercel.com/templates/nuxt/chat-sdk-knowledge-agent) +- **Community Agent** — Open source AI-powered Slack community management bot with a built-in Next.js admin panel. Uses Chat SDK, AI SDK, and Vercel Workflow. (https://vercel.com/templates/next.js/chat-sdk-community-agent) + + + +## Quick start + +```typescript +import { Chat } from "chat"; +import { createSlackAdapter } from "@chat-adapter/slack"; +import { createRedisState } from "@chat-adapter/state-redis"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + slack: createSlackAdapter(), + }, + state: createRedisState(), + dedupeTtlMs: 600_000, +}); + +bot.onNewMention(async (thread) => { + await thread.subscribe(); + await thread.post("Hello! I'm listening to this thread."); +}); + +bot.onSubscribedMessage(async (thread, message) => { + await thread.post(`You said: ${message.text}`); +}); +``` + +## Core concepts + +- **Chat** — main entry point; coordinates adapters, routing, locks, and state +- **Adapters** — platform-specific integrations for Slack, Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp +- **State adapters** — persistence for subscriptions, locks, dedupe, and thread state +- **Thread** — conversation context with `post()`, `stream()`, `subscribe()`, `setState()`, `startTyping()` +- **Message** — normalized content with `text`, `formatted`, attachments, author info, and platform `raw` +- **Channel** — container for threads and top-level posts + +## Event handlers + +| Handler | Trigger | +|---------|---------| +| `onNewMention` | Bot @-mentioned in an unsubscribed thread | +| `onDirectMessage` | New DM in an unsubscribed DM thread | +| `onSubscribedMessage` | Any message in a subscribed thread | +| `onNewMessage(regex)` | Regex match in an unsubscribed thread | +| `onReaction(emojis?)` | Emoji added or removed | +| `onAction(actionIds?)` | Button clicks and select/radio interactions | +| `onModalSubmit(callbackId?)` | Modal form submitted | +| `onModalClose(callbackId?)` | Modal dismissed/cancelled | +| `onSlashCommand(commands?)` | Slash command invocation | +| `onAssistantThreadStarted` | Slack assistant thread opened | +| `onAssistantContextChanged` | Slack assistant context changed | +| `onAppHomeOpened` | Slack App Home opened | +| `onMemberJoinedChannel` | Slack member joined channel event | + +Read `node_modules/chat/docs/handling-events.mdx`, `node_modules/chat/docs/actions.mdx`, `node_modules/chat/docs/modals.mdx`, and `node_modules/chat/docs/slash-commands.mdx` before wiring handlers. `onDirectMessage` behavior is documented in `node_modules/chat/docs/direct-messages.mdx`. + +## Streaming + +Pass any `AsyncIterable` to `thread.post()`. For AI SDK, prefer `result.fullStream` over `result.textStream` when available so step boundaries are preserved. + +```typescript +import { ToolLoopAgent } from "ai"; + +const agent = new ToolLoopAgent({ model: "anthropic/claude-4.5-sonnet" }); + +bot.onNewMention(async (thread, message) => { + const result = await agent.stream({ prompt: message.text }); + await thread.post(result.fullStream); +}); +``` + +Key details: +- `streamingUpdateIntervalMs` controls post+edit fallback cadence +- `fallbackStreamingPlaceholderText` defaults to `"..."`; set `null` to disable +- Structured `StreamChunk` support is Slack-only; other adapters ignore non-text chunks + +## Cards and modals (JSX) + +Set `jsxImportSource: "chat"` in `tsconfig.json`. + +Card components: +- `Card`, `CardText`, `Section`, `Fields`, `Field`, `Button`, `CardLink`, `LinkButton`, `Actions`, `Select`, `SelectOption`, `RadioSelect`, `Table`, `Image`, `Divider` + +Modal components: +- `Modal`, `TextInput`, `Select`, `SelectOption`, `RadioSelect` + +```tsx +await thread.post( + + Your order has been received. + + + + + +); +``` + +## Adapter inventory + +See [chat-sdk.dev/adapters](https://chat-sdk.dev/adapters) for the current list of official, vendor-official, and community adapters, including package names and authors. For the exact factory function and config types of an installed adapter, inspect its `dist/index.d.ts` in `node_modules`. + +## Building a custom adapter + +Read these published docs first: +- `node_modules/chat/docs/contributing/building.mdx` +- `node_modules/chat/docs/contributing/testing.mdx` +- `node_modules/chat/docs/contributing/publishing.mdx` + +Also inspect: +- `node_modules/chat/dist/index.d.ts` — `Adapter` and related interfaces +- `node_modules/@chat-adapter/shared/dist/index.d.ts` — shared errors and utilities +- Installed official adapter `dist/index.d.ts` files — reference implementations for config and APIs + +A custom adapter needs request verification, webhook parsing, message/thread/channel operations, ID encoding/decoding, and a format converter. Use `BaseFormatConverter` from `chat` and shared utilities from `@chat-adapter/shared`. + +## Webhook setup + +Each registered adapter exposes `bot.webhooks.`. Wire those directly to your HTTP framework routes. See `node_modules/chat/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md` and `node_modules/chat/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md` for framework-specific route patterns. diff --git a/.agents/skills/convex-create-component/SKILL.md b/.agents/skills/convex-create-component/SKILL.md index 22af601f..64bd42f9 100644 --- a/.agents/skills/convex-create-component/SKILL.md +++ b/.agents/skills/convex-create-component/SKILL.md @@ -1,6 +1,6 @@ --- name: convex-create-component -description: Designs and builds Convex components with isolated tables, clear boundaries, and app-facing wrappers. Use this skill when creating a new Convex component, extracting reusable backend logic into a component, building a third-party integration that owns its own tables, packaging Convex functionality for reuse, or when the user mentions defineComponent, app.use, ComponentApi, ctx.runQuery/runMutation across component boundaries, or wants to separate concerns into isolated Convex modules. +description: Builds reusable Convex components with isolated tables and app-facing APIs. Use for new components, reusable backend modules, integrations, or component boundary work. --- # Convex Create Component diff --git a/.agents/skills/convex-migration-helper/SKILL.md b/.agents/skills/convex-migration-helper/SKILL.md index db36c622..4a4ed167 100644 --- a/.agents/skills/convex-migration-helper/SKILL.md +++ b/.agents/skills/convex-migration-helper/SKILL.md @@ -1,6 +1,6 @@ --- name: convex-migration-helper -description: Plans and executes safe Convex schema and data migrations using the widen-migrate-narrow workflow and the @convex-dev/migrations component. Use this skill when a deployment fails schema validation, existing documents need backfilling, fields need adding or removing or changing type, tables need splitting or merging, or a zero-downtime migration strategy is needed. Also use when the user mentions breaking schema changes, multi-deploy rollouts, or data transformations on existing Convex tables. +description: Plans Convex schema and data migrations with widen-migrate-narrow and @convex-dev/migrations. Use for breaking schema changes, backfills, table reshaping, or zero-downtime rollouts. --- # Convex Migration Helper diff --git a/.agents/skills/convex-performance-audit/SKILL.md b/.agents/skills/convex-performance-audit/SKILL.md index 382951cf..f2554dca 100644 --- a/.agents/skills/convex-performance-audit/SKILL.md +++ b/.agents/skills/convex-performance-audit/SKILL.md @@ -1,6 +1,6 @@ --- name: convex-performance-audit -description: Audits and optimizes Convex application performance across hot-path reads, write contention, subscription cost, and function limits. Use this skill when a Convex feature is slow or expensive, npx convex insights shows high bytes or documents read, OCC conflict errors or mutation retries appear, subscriptions or UI updates are costly, functions hit execution or transaction limits, or the user mentions performance, latency, read amplification, or invalidation problems in a Convex app. +description: Audits Convex performance for reads, subscriptions, write contention, and function limits. Use for slow features, insights findings, OCC conflicts, or read amplification. --- # Convex Performance Audit diff --git a/.agents/skills/convex-quickstart/SKILL.md b/.agents/skills/convex-quickstart/SKILL.md index 5bff17bc..f506b3e4 100644 --- a/.agents/skills/convex-quickstart/SKILL.md +++ b/.agents/skills/convex-quickstart/SKILL.md @@ -1,6 +1,6 @@ --- name: convex-quickstart -description: Initializes a new Convex project from scratch or adds Convex to an existing app. Use this skill when starting a new project with Convex, scaffolding with npm create convex@latest, adding Convex to an existing React, Next.js, Vue, Svelte, or other frontend, wiring up ConvexProvider, configuring environment variables for the deployment URL, or running npx convex dev for the first time, even if the user just says "set up Convex" or "add a backend." +description: Creates or adds Convex to an app. Use for new Convex projects, npm create convex@latest, frontend setup, env vars, or the first npx convex dev run. --- # Convex Quickstart diff --git a/.agents/skills/convex-setup-auth/SKILL.md b/.agents/skills/convex-setup-auth/SKILL.md index 0d1d9dd6..59a92285 100644 --- a/.agents/skills/convex-setup-auth/SKILL.md +++ b/.agents/skills/convex-setup-auth/SKILL.md @@ -1,6 +1,6 @@ --- name: convex-setup-auth -description: Sets up Convex authentication with user management, identity mapping, and access control. Use this skill when adding login or signup to a Convex app, configuring Convex Auth, Clerk, WorkOS AuthKit, Auth0, or custom JWT providers, wiring auth.config.ts, protecting queries and mutations with ctx.auth.getUserIdentity(), creating a users table with identity mapping, or setting up role-based access control, even if the user just says "add auth" or "make it require login." +description: Sets up Convex auth, identity mapping, and access control. Use for login, auth providers, users tables, protected functions, or roles in a Convex app. --- # Convex Authentication Setup diff --git a/.agents/skills/convex/SKILL.md b/.agents/skills/convex/SKILL.md index d4678270..9e8ebbd8 100644 --- a/.agents/skills/convex/SKILL.md +++ b/.agents/skills/convex/SKILL.md @@ -1,6 +1,6 @@ --- name: convex -description: Routing skill for Convex work in this repo. Use when the user explicitly invokes the `convex` skill, asks which Convex workflow or skill to use, or says they are working on a Convex app without naming a specific task yet. Do not prefer this skill when the request is clearly about setting up Convex, authentication, components, migrations, or performance. +description: Routes general Convex requests to the right project skill. Use when the user asks which Convex skill to use or gives an underspecified Convex app task. --- # Convex diff --git a/.claude/skills/chat-sdk b/.claude/skills/chat-sdk new file mode 120000 index 00000000..77d22f1b --- /dev/null +++ b/.claude/skills/chat-sdk @@ -0,0 +1 @@ +../../.agents/skills/chat-sdk \ No newline at end of file diff --git a/.env.example b/.env.example index 82a01638..9009c4d7 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,10 @@ SENDBLUE_API_KEY= SENDBLUE_API_SECRET= SENDBLUE_FROM_NUMBER= +# ---- Telegram Bot ---- +# Create a bot with BotFather, get the token, and paste it here to enable +TELEGRAM_BOT_TOKEN= + # ---- Claude model ---- # Uses your Claude Code subscription automatically — no separate API key needed. # Override with ANTHROPIC_API_KEY if you want to bypass the subscription. diff --git a/README.md b/README.md index 292ac600..f562068b 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,79 @@ Visit `http://localhost:5173` for the debug dashboard (chat, agents, memory, eve --- +## Adding more chat platforms + +Boop uses [Chat SDK](https://chat-sdk.dev) under the hood. iMessage (Sendblue) and Telegram ship out of the box — adding WhatsApp, Slack, Discord, Teams, or any other supported platform is a one-file change. + +### How it works + +All adapters are registered in [`server/bot.ts`](server/bot.ts). Each call to `registerIfConfigured` adds a platform: it checks for the required env vars, creates the adapter, and mounts a webhook route automatically. No changes needed anywhere else. + +```typescript +// server/bot.ts — add this block for your new platform +registerIfConfigured( + "slack", // adapter name (must match adapter's .name) + "/slack/webhook", // webhook path Express will mount + ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], // env vars that must be set + () => createSlackAdapter(), // factory called only when vars are present +); +``` + +`npm run dev` picks up the new route automatically and prints it in the startup output: + +``` +server │ [bot] registered adapter: slack → POST /slack/webhook +``` + +### Supported platforms + +| Platform | Package | Required env vars | +|---|---|---| +| iMessage (Sendblue) | `chat-adapter-sendblue` | `SENDBLUE_API_KEY`, `SENDBLUE_API_SECRET`, `SENDBLUE_FROM_NUMBER` | +| Telegram | `@chat-adapter/telegram` | `TELEGRAM_BOT_TOKEN` | +| Slack | `@chat-adapter/slack` | `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET` | +| WhatsApp (via Twilio) | `@chat-adapter/whatsapp` | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_WHATSAPP_NUMBER` | +| Discord | `@chat-adapter/discord` | `DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY` | +| Microsoft Teams | `@chat-adapter/teams` | `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD` | +| Google Chat | `@chat-adapter/google-chat` | `GOOGLE_CHAT_SERVICE_ACCOUNT_KEY` | +| GitHub | `@chat-adapter/github` | `GITHUB_APP_ID`, `GITHUB_PRIVATE_KEY`, `GITHUB_WEBHOOK_SECRET` | +| Linear | `@chat-adapter/linear` | `LINEAR_WEBHOOK_SECRET` | + +See the [Chat SDK adapter directory](https://chat-sdk.dev/adapters) for the full list and exact config options. + +### Step-by-step: adding Slack + +1. Install the adapter: + ```bash + npm install @chat-adapter/slack + ``` + +2. Add env vars to `.env.local`: + ``` + SLACK_BOT_TOKEN=xoxb-... + SLACK_SIGNING_SECRET=... + ``` + +3. Add one block to `server/bot.ts`: + ```typescript + import { createSlackAdapter } from "@chat-adapter/slack"; + + registerIfConfigured( + "slack", + "/slack/webhook", + ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], + () => createSlackAdapter(), + ); + ``` + +4. Point your Slack app's Event Subscriptions URL to `/slack/webhook`. + +5. Restart `npm run dev`. The agent now responds on both iMessage and Slack using the same logic. + +All platforms share the same handlers, memory, automations, and Composio integrations — everything just works. + +--- + ## Architecture in 30 seconds ``` diff --git a/package-lock.json b/package-lock.json index 28c33860..76a6fc00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,22 @@ "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", + "@anthropic-ai/sdk": "^0.91.1", + "@chat-adapter/state-memory": "^4.26.0", + "@chat-adapter/telegram": "^4.26.0", "@composio/claude-agent-sdk": "^0.6.10", "@composio/core": "^0.6.10", "@hugeicons/core-free-icons": "^1.0.0", "@hugeicons/react": "^1.0.0", "@modelcontextprotocol/sdk": "^1.0.0", + "chat": "^4.26.0", + "chat-adapter-sendblue": "^0.2.0", "convex": "^1.17.0", "cors": "^2.8.5", "croner": "^9.0.0", "dotenv": "^16.4.0", "express": "^5.0.0", + "openai": "^6.34.0", "react-force-graph-2d": "^1.27.0", "ws": "^8.18.0", "zod": "^3.23.0" @@ -69,6 +75,26 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -323,6 +349,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -371,6 +406,34 @@ "node": ">=6.9.0" } }, + "node_modules/@chat-adapter/shared": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/shared/-/shared-4.26.0.tgz", + "integrity": "sha512-YD0MGktFXrArUqTBsyPfL5vkdD1WBS58PAWO0oVrMQAMmPxpAXfWGjBtZCkf3y8R8Svb0uVuVXiMZSForaEnMQ==", + "license": "MIT", + "dependencies": { + "chat": "4.26.0" + } + }, + "node_modules/@chat-adapter/state-memory": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/state-memory/-/state-memory-4.26.0.tgz", + "integrity": "sha512-FsfyM/A9Bf1yFc1FWmOsK+a4YVwm5FogX25hZxFG6cEvyFb6Cd924SsbtvF06yItY/7J2UFetCsMmBPkdPKshQ==", + "license": "MIT", + "dependencies": { + "chat": "4.26.0" + } + }, + "node_modules/@chat-adapter/telegram": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/telegram/-/telegram-4.26.0.tgz", + "integrity": "sha512-PE2HoCQ4648VNKZTuHFanQNoYzM/niNoSbDyYlPq6VOoB5qsoo1ctR8TERyl1EfPBNexWZpSWYrrnQPr15LUfA==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.26.0", + "chat": "4.26.0" + } + }, "node_modules/@composio/claude-agent-sdk": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/@composio/claude-agent-sdk/-/claude-agent-sdk-0.6.10.tgz", @@ -1871,6 +1934,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1916,6 +1988,21 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -1992,6 +2079,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -2023,6 +2116,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@workflow/serde": { + "version": "4.1.0-beta.2", + "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0-beta.2.tgz", + "integrity": "sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==", + "license": "Apache-2.0" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2158,6 +2257,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2347,6 +2456,16 @@ "node": ">=12" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2363,6 +2482,49 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chat": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/chat/-/chat-4.26.0.tgz", + "integrity": "sha512-QToDnIEGpyb8yQA6YLMHOSRK30YVk4RtsyFyuWFYyB2c4jQlyIrSWtwVK7qyvmvqzQp9uDwCdJRAhS8GtCHAGQ==", + "license": "MIT", + "dependencies": { + "@workflow/serde": "4.1.0-beta.2", + "mdast-util-to-string": "^4.0.0", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "remend": "^1.2.1", + "unified": "^11.0.5" + } + }, + "node_modules/chat-adapter-sendblue": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chat-adapter-sendblue/-/chat-adapter-sendblue-0.2.0.tgz", + "integrity": "sha512-JKqRa5KyoBsndRrq2LuGhob1uCdO1goWAbqaSLCld3KE9q288CnEo999GdWuDVWa9yqt1uf5F9/Oe3iFkSrW2w==", + "license": "MIT", + "dependencies": { + "chat": "^4.23.0", + "sendblue": "^3.6.1" + }, + "peerDependencies": { + "chat": ">=4.0.0" + }, + "peerDependenciesMeta": { + "chat": { + "optional": false + } + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2828,6 +2990,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2873,6 +3048,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2883,6 +3067,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -3246,6 +3443,12 @@ "express": ">= 4.11" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3981,6 +4184,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4199,6 +4414,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4529,6 +4757,16 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4561,6 +4799,16 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4570,6 +4818,207 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -4600,23 +5049,586 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" - }, + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, "engines": { "node": ">=18" }, @@ -5381,6 +6393,61 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remend": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/remend/-/remend-1.3.0.tgz", + "integrity": "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==", + "license": "Apache-2.0" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5589,6 +6656,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/sendblue": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sendblue/-/sendblue-3.8.0.tgz", + "integrity": "sha512-jG47SQsZpWjVOxbX9NGeP9Rh15L9rKzoJDp5APSEXbw91y1aNJFQ7pbVMchr8g2nC0tldbbmKqoXgZa1Aum9oQ==", + "license": "Apache-2.0" + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -6011,6 +7084,22 @@ "node": ">=0.6" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -6169,6 +7258,80 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6229,6 +7392,34 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", @@ -6943,6 +8134,16 @@ "peerDependencies": { "zod": "^3.25.28 || ^4" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 4696eb23..e52f1301 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -137,24 +137,26 @@ async function waitForNgrokUrl(timeoutMs = 15000) { function showBanner(url, stable) { const line = "═".repeat(68); - const webhook = `${url}/sendblue/webhook`; const dashboard = `http://localhost:5173`; - const from = envVars.SENDBLUE_FROM_NUMBER; - const fromLine = from - ? ` 📱 Text this Sendblue number: ${from} (from a DIFFERENT phone)` - : ` ⚠ SENDBLUE_FROM_NUMBER is not set — outbound sends will fail.\n Run: npm run sendblue:sync (pulls it from the Sendblue CLI)`; + + // Build per-platform webhook lines dynamically + const platformLines = []; + if (envVars.SENDBLUE_API_KEY && envVars.SENDBLUE_API_SECRET && envVars.SENDBLUE_FROM_NUMBER) { + platformLines.push(` 📮 Sendblue webhook: ${url}/sendblue/webhook`); + platformLines.push(` 📱 Text this number: ${envVars.SENDBLUE_FROM_NUMBER} (from a DIFFERENT phone)`); + } + if (envVars.TELEGRAM_BOT_TOKEN) { + platformLines.push(` ✈️ Telegram webhook: ${url}/telegram/webhook`); + } const headline = stable ? `your STABLE public URL is live.` - : `ngrok tunnel is live (webhook auto-registered with Sendblue).`; + : `ngrok tunnel is live (webhooks auto-registered).`; const footer = stable ? `` - : `\n${C.dim} ℹ The inbound webhook above was registered with Sendblue automatically. - Set SENDBLUE_AUTO_WEBHOOK=false in .env.local to disable, or pick a - stable URL (ngrok paid / Cloudflare Tunnel) via \`npm run setup\`.${C.reset}\n`; - const guide = stable - ? `\n → First time? Sendblue dashboard → API Settings → Webhook\n Configuration → add ${webhook} as INBOUND MESSAGE.\n` - : ``; + : `\n${C.dim} ℹ Webhooks above were registered automatically on boot. + Set SENDBLUE_AUTO_WEBHOOK=false in .env.local to disable Sendblue auto-register. + Use a stable URL (ngrok paid / Cloudflare Tunnel) via \`npm run setup\`.${C.reset}\n`; console.log(` ${C.banner}${line} @@ -162,8 +164,7 @@ ${C.banner}${line} 🐶 Debug dashboard (click me): ${dashboard} 🌐 Public URL: ${url} - 📮 Sendblue webhook (inbound): ${webhook} -${fromLine}${guide} +${platformLines.join("\n")} ${line}${C.reset}${footer}`); } @@ -225,11 +226,9 @@ if (useNgrok && ngrokInstalled) { // Wait for all the core services to be ready before printing the banner, // so the URL isn't dangled in front of the user while Convex is still booting. -async function autoRegisterWebhook(publicUrl) { - if (envVars.SENDBLUE_AUTO_WEBHOOK === "false") return; - const webhookUrl = `${publicUrl}/sendblue/webhook`; - const prefix = `${C.ngrok}webhook${C.reset} │ `; - const child = spawn("node", ["scripts/sendblue-webhook.mjs", webhookUrl], { +function runWebhookScript(script, args, label) { + const prefix = `${C.ngrok}${label.padEnd(8)}${C.reset} │ `; + const child = spawn("node", [script, ...args], { cwd: root, env: { ...process.env }, }); @@ -243,7 +242,33 @@ async function autoRegisterWebhook(publicUrl) { if (line.trim()) process.stdout.write(prefix + line + "\n"); } }); - await new Promise((r) => child.on("exit", r)); + return new Promise((r) => child.on("exit", r)); +} + +async function autoRegisterWebhook(publicUrl) { + const tasks = []; + + if (envVars.SENDBLUE_AUTO_WEBHOOK !== "false") { + tasks.push( + runWebhookScript( + "scripts/sendblue-webhook.mjs", + [`${publicUrl}/sendblue/webhook`], + "sendblue", + ), + ); + } + + if (envVars.TELEGRAM_BOT_TOKEN) { + tasks.push( + runWebhookScript( + "scripts/telegram-webhook.mjs", + [`${publicUrl}/telegram/webhook`], + "telegram", + ), + ); + } + + await Promise.all(tasks); } Promise.all([ diff --git a/scripts/telegram-webhook.mjs b/scripts/telegram-webhook.mjs new file mode 100644 index 00000000..71187430 --- /dev/null +++ b/scripts/telegram-webhook.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node +// Register (or clear) the Telegram webhook for this bot. +// Usage: +// node scripts/telegram-webhook.mjs — register +// node scripts/telegram-webhook.mjs --clear — delete webhook + +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, ".."); + +function readEnv() { + const p = resolve(root, ".env.local"); + if (!existsSync(p)) return {}; + const env = {}; + for (const line of readFileSync(p, "utf8").split("\n")) { + const m = line.match(/^([A-Z0-9_]+)=(.*?)(?:\s+#.*)?$/); + if (m) env[m[1]] = m[2].trim(); + } + return env; +} + +const env = readEnv(); +const token = process.env.TELEGRAM_BOT_TOKEN || env.TELEGRAM_BOT_TOKEN; + +if (!token) { + console.error("TELEGRAM_BOT_TOKEN not set in .env.local — skipping webhook registration."); + process.exit(0); +} + +const arg = process.argv[2]; +const apiBase = `https://api.telegram.org/bot${token}`; + +if (arg === "--clear") { + const res = await fetch(`${apiBase}/deleteWebhook`, { method: "POST" }); + const data = await res.json(); + console.log(data.ok ? "Telegram webhook cleared." : `Failed: ${data.description}`); + process.exit(data.ok ? 0 : 1); +} + +if (!arg || !arg.startsWith("http")) { + console.error("Usage: node scripts/telegram-webhook.mjs "); + process.exit(1); +} + +const webhookUrl = arg; +const res = await fetch(`${apiBase}/setWebhook`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: webhookUrl, + allowed_updates: ["message", "edited_message", "callback_query"], + drop_pending_updates: true, + }), +}); +const data = await res.json(); + +if (data.ok) { + console.log(`Telegram webhook registered: ${webhookUrl}`); +} else { + console.error(`Telegram webhook registration failed: ${data.description}`); + process.exit(1); +} diff --git a/server/automations.ts b/server/automations.ts index d68cad33..e77c0e4b 100644 --- a/server/automations.ts +++ b/server/automations.ts @@ -2,7 +2,7 @@ import { Cron } from "croner"; import { api } from "../convex/_generated/api.js"; import { convex } from "./convex-client.js"; import { spawnExecutionAgent } from "./execution-agent.js"; -import { sendImessage } from "./sendblue.js"; +import { bot } from "./bot.js"; import { broadcast } from "./broadcast.js"; function randomId(prefix: string): string { @@ -59,10 +59,14 @@ async function runAutomation(a: { }); if (a.notifyConversationId && res.result) { - if (a.notifyConversationId.startsWith("sms:")) { - const number = a.notifyConversationId.slice(4); - const preamble = `[${a.name}]\n\n`; - await sendImessage(number, preamble + res.result); + // Post via Chat SDK — works for any registered adapter (sms, slack, etc.) + // Normalise legacy "sms:" prefix stored before Sendblue adapter rename. + const dmTarget = a.notifyConversationId.replace(/^sms:/, "sendblue:"); + try { + const dm = await bot.openDM(dmTarget); + await dm.post(`[${a.name}]\n\n${res.result}`); + } catch (err) { + console.error(`[automations] failed to notify ${dmTarget}`, err); } await convex.mutation(api.messages.send, { conversationId: a.notifyConversationId, diff --git a/server/bot.ts b/server/bot.ts new file mode 100644 index 00000000..ec1e981d --- /dev/null +++ b/server/bot.ts @@ -0,0 +1,142 @@ +import { Chat, type Adapter, type Thread, type Message as ChatMessage } from "chat"; +import { createMemoryState } from "@chat-adapter/state-memory"; +import { createSendblueAdapter } from "chat-adapter-sendblue"; +import { createTelegramAdapter } from "@chat-adapter/telegram"; +import { handleUserMessage } from "./interaction-agent.js"; +import { convex } from "./convex-client.js"; +import { api } from "../convex/_generated/api.js"; +import { broadcast } from "./broadcast.js"; + +// --------------------------------------------------------------------------- +// Adapter registry — add new platforms here (or via env-driven registration). +// Each entry: { envCheck, factory, webhookPath } +// --------------------------------------------------------------------------- + +const adapters: Record = {}; +const webhookPaths: Record = {}; + +function registerIfConfigured( + name: string, + webhookPath: string, + envKeys: string[], + factory: () => Adapter, +): void { + if (envKeys.every((k) => !!process.env[k])) { + adapters[name] = factory(); + webhookPaths[name] = webhookPath; + console.log(`[bot] registered adapter: ${name} → POST ${webhookPath}`); + } else { + console.log(`[bot] skipping adapter: ${name} (missing env: ${envKeys.filter((k) => !process.env[k]).join(", ")})`); + } +} + +// SendBlue / iMessage + Android RCS/SMS +// Key MUST match SendblueAdapter.name ("sendblue") so bot.webhooks.sendblue resolves. +registerIfConfigured("sendblue", "/sendblue/webhook", ["SENDBLUE_API_KEY", "SENDBLUE_API_SECRET", "SENDBLUE_FROM_NUMBER"], () => + createSendblueAdapter({ + apiKey: process.env.SENDBLUE_API_KEY!, + apiSecret: process.env.SENDBLUE_API_SECRET!, + defaultFromNumber: process.env.SENDBLUE_FROM_NUMBER!, + allowedServices: ["iMessage", "SMS", "RCS", "sms"], + }), +); + +// Telegram +registerIfConfigured("telegram", "/telegram/webhook", ["TELEGRAM_BOT_TOKEN"], () => + createTelegramAdapter({ mode: "webhook" }), +); + +// Add more platforms by appending registerIfConfigured() calls, e.g.: +// registerIfConfigured("slack", "/slack/webhook", ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"], () => +// createSlackAdapter()); + +// --------------------------------------------------------------------------- + +export const bot = new Chat({ + userName: "boop", + adapters, + state: createMemoryState(), + dedupeTtlMs: 600_000, +}); + +/** Map of adapter name → Express webhook path, for mounting in index.ts */ +export { webhookPaths }; + +// --------------------------------------------------------------------------- +// Shared turn handler +// --------------------------------------------------------------------------- + +async function handleTurn(thread: Thread, message: ChatMessage): Promise { + if (message.author.isMe) return; + await thread.subscribe(); + + const conversationId = thread.id; + const content = message.text; + const turnTag = Math.random().toString(36).slice(2, 8); + const preview = content.length > 100 ? content.slice(0, 100) + "…" : content; + + console.log(`[turn ${turnTag}] ← ${conversationId}: ${JSON.stringify(preview)}`); + broadcast("message_in", { conversationId, content, from: message.author.userId }); + + const start = Date.now(); + thread.startTyping().catch(() => {}); + const typingLoop = setInterval(() => thread.startTyping().catch(() => {}), 5000); + + try { + const reply = await handleUserMessage({ + conversationId, + content, + turnTag, + onThinking: (t) => broadcast("thinking", { conversationId, t }), + onSendAck: async (ack) => { + await thread.post(ack); + await convex.mutation(api.messages.send, { + conversationId, + role: "assistant", + content: ack, + }); + broadcast("assistant_ack", { conversationId, content: ack }); + }, + }); + + if (reply) { + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + const replyPreview = reply.length > 100 ? reply.slice(0, 100) + "…" : reply; + console.log( + `[turn ${turnTag}] → reply (${elapsed}s, ${reply.length} chars): ${JSON.stringify(replyPreview)}`, + ); + await thread.post(reply); + await convex.mutation(api.messages.send, { + conversationId, + role: "assistant", + content: reply, + }); + } else { + console.log(`[turn ${turnTag}] → (no reply)`); + } + } catch (err) { + console.error(`[turn ${turnTag}] handler error`, err); + } finally { + clearInterval(typingLoop); + } +} + +// --------------------------------------------------------------------------- +// Event routing +// --------------------------------------------------------------------------- + +// DMs on platforms that implement isDM (Telegram, Slack, Discord, Teams, etc.) +bot.onDirectMessage(async (thread, message) => handleTurn(thread, message)); + +// @-mentions in channels (Slack, Discord, Teams, Google Chat) +bot.onNewMention(async (thread, message) => handleTurn(thread, message)); + +// Sendblue fallback: adapter doesn't implement isDM so onDirectMessage never fires. +// Scoped to sendblue only to prevent double-firing alongside onDirectMessage/onNewMention. +bot.onNewMessage(/[\s\S]*/, async (thread, message) => { + if (thread.adapter.name !== "sendblue") return; + await handleTurn(thread, message); +}); + +// Subscribed threads (follow-up messages after first interaction) +bot.onSubscribedMessage(async (thread, message) => handleTurn(thread, message)); diff --git a/server/index.ts b/server/index.ts index 2f3444ba..677a8165 100644 --- a/server/index.ts +++ b/server/index.ts @@ -4,7 +4,7 @@ import cors from "cors"; import { createServer } from "node:http"; import { WebSocketServer } from "ws"; import { addClient } from "./broadcast.js"; -import { createSendblueRouter } from "./sendblue.js"; +import { bot, webhookPaths } from "./bot.js"; import { handleUserMessage } from "./interaction-agent.js"; import { loadIntegrations } from "./integrations/registry.js"; import { startCleanupLoop } from "./memory/clean.js"; @@ -14,6 +14,44 @@ import { startConsolidationLoop } from "./consolidation.js"; import { cancelAgent, retryAgent } from "./execution-agent.js"; import { createComposioRouter } from "./composio-routes.js"; +const DEBUG_WEBHOOKS = process.env.DEBUG_WEBHOOKS === "true"; + +/** Bridge Express req/res to Web API Request/Response (used by Chat SDK webhooks) */ +async function bridgeWebhook( + name: string, + req: express.Request & { rawBody?: Buffer }, + res: express.Response, + handler: ((r: Request, opts?: { waitUntil?: (p: Promise) => void }) => Promise) | undefined, +): Promise { + if (!handler) { + console.error(`[webhook:${name}] handler not found on bot.webhooks`); + res.status(500).json({ error: `no handler for adapter "${name}"` }); + return; + } + const url = `http://localhost${req.originalUrl}`; + // Forward all incoming headers so adapter verification logic (e.g. x-webhook-secret) works + const headers: Record = {}; + for (const [k, v] of Object.entries(req.headers)) { + if (typeof v === "string") headers[k] = v; + } + // Preserve original bytes for HMAC signature verification (Slack, GitHub, Discord, etc.) + const body = req.rawBody?.toString("utf-8") ?? JSON.stringify(req.body); + if (DEBUG_WEBHOOKS) { + console.log(`[webhook:${name}] incoming POST ${body.length}b`); + } + const webReq = new Request(url, { method: req.method, headers, body }); + const webRes = await handler(webReq); + if (DEBUG_WEBHOOKS) { + console.log(`[webhook:${name}] handler status=${webRes.status}`); + } + // Proxy response faithfully — some adapters return plain text (URL verification challenges) + for (const [k, v] of webRes.headers.entries()) { + res.setHeader(k, v); + } + const responseBody = Buffer.from(await webRes.arrayBuffer()); + res.status(webRes.status).send(responseBody); +} + async function main() { await loadIntegrations(); startCleanupLoop(); @@ -23,13 +61,29 @@ async function main() { const app = express(); app.use(cors()); - app.use(express.json({ limit: "2mb" })); + // Capture raw body bytes before parsing so HMAC signature verification works in all adapters + app.use(express.json({ + limit: "2mb", + verify: (req: express.Request & { rawBody?: Buffer }, _res, buf) => { + req.rawBody = buf; + }, + })); app.get("/health", (_req, res) => { res.json({ ok: true, service: "boop-agent" }); }); - app.use("/sendblue", createSendblueRouter()); + // Mount webhook routes for all registered chat adapters + for (const [name, path] of Object.entries(webhookPaths)) { + const handler = (bot.webhooks as Record Promise>)[name]; + app.post(path, (req, res) => { + bridgeWebhook(name, req, res, handler).catch((err) => { + console.error(`[${name}] webhook error`, err); + if (!res.headersSent) res.status(500).json({ error: String(err) }); + }); + }); + } + app.use("/composio", createComposioRouter()); app.post("/agents/:id/cancel", (req, res) => { @@ -40,7 +94,6 @@ async function main() { app.post("/consolidate", async (_req, res) => { try { const { runConsolidation } = await import("./consolidation.js"); - // Fire-and-forget so the HTTP request returns immediately. runConsolidation("manual").catch((err) => console.error("[consolidation] manual run failed", err), ); @@ -87,7 +140,9 @@ async function main() { console.log(`boop-agent server listening on :${port}`); console.log(` health GET http://localhost:${port}/health`); console.log(` chat POST http://localhost:${port}/chat`); - console.log(` sendblue POST http://localhost:${port}/sendblue/webhook`); + for (const [name, path] of Object.entries(webhookPaths)) { + console.log(` ${name.padEnd(12)}POST http://localhost:${port}${path}`); + } console.log(` websocket WS ws://localhost:${port}/ws`); }); } diff --git a/server/interaction-agent.ts b/server/interaction-agent.ts index 18ca3d36..79e0f6a8 100644 --- a/server/interaction-agent.ts +++ b/server/interaction-agent.ts @@ -8,7 +8,6 @@ import { availableIntegrations, spawnExecutionAgent } from "./execution-agent.js import { createAutomationMcp } from "./automation-tools.js"; import { createDraftDecisionMcp } from "./draft-tools.js"; import { broadcast } from "./broadcast.js"; -import { sendImessage } from "./sendblue.js"; import { aggregateUsageFromResult, EMPTY_USAGE, type UsageTotals } from "./usage.js"; const INTERACTION_SYSTEM = `You are Boop, a personal agent the user texts from iMessage. @@ -94,6 +93,7 @@ interface HandleOpts { content: string; turnTag?: string; onThinking?: (chunk: string) => void; + onSendAck?: (text: string) => Promise; } function randomId(prefix: string): string { @@ -133,9 +133,9 @@ export async function handleUserMessage(opts: HandleOpts): Promise { content: [{ type: "text" as const, text: "Empty ack skipped." }], }; } - if (opts.conversationId.startsWith("sms:")) { - const number = opts.conversationId.slice(4); - await sendImessage(number, text); + if (opts.onSendAck) { + await opts.onSendAck(text); + return { content: [{ type: "text" as const, text: "Ack sent to user." }] }; } await convex.mutation(api.messages.send, { conversationId: opts.conversationId, diff --git a/server/sendblue.ts b/server/sendblue.ts deleted file mode 100644 index ec6ddf3c..00000000 --- a/server/sendblue.ts +++ /dev/null @@ -1,182 +0,0 @@ -import express from "express"; -import { api } from "../convex/_generated/api.js"; -import { convex } from "./convex-client.js"; -import { handleUserMessage } from "./interaction-agent.js"; -import { broadcast } from "./broadcast.js"; - -const API_BASE = "https://api.sendblue.com/api"; -const MAX_CHUNK = 2900; - -function stripMarkdown(text: string): string { - return text - .replace(/```[\s\S]*?```/g, (m) => m.replace(/```\w*\n?|```/g, "")) - .replace(/\*\*(.+?)\*\*/g, "$1") - .replace(/\*(.+?)\*/g, "$1") - .replace(/`([^`]+)`/g, "$1") - .replace(/^#+\s+/gm, "") - .replace(/\[(.+?)\]\((.+?)\)/g, "$1 ($2)") - .trim(); -} - -function chunk(text: string, size = MAX_CHUNK): string[] { - if (text.length <= size) return [text]; - const out: string[] = []; - let buf = ""; - for (const line of text.split(/\n/)) { - if ((buf + "\n" + line).length > size) { - if (buf) out.push(buf); - buf = line; - } else { - buf = buf ? buf + "\n" + line : line; - } - } - if (buf) out.push(buf); - return out; -} - -function headers(): Record | null { - const apiKey = process.env.SENDBLUE_API_KEY; - const apiSecret = process.env.SENDBLUE_API_SECRET; - if (!apiKey || !apiSecret) return null; - return { - "Content-Type": "application/json", - "sb-api-key-id": apiKey, - "sb-api-secret-key": apiSecret, - }; -} - -function normalizeE164(n: string | undefined): string | undefined { - if (!n) return undefined; - const trimmed = n.trim(); - if (!trimmed) return undefined; - if (trimmed.startsWith("+")) return trimmed; - // Bare US-length numbers get a +1. Longer/shorter just get a leading +. - if (/^\d{10}$/.test(trimmed)) return `+1${trimmed}`; - if (/^\d{11,15}$/.test(trimmed)) return `+${trimmed}`; - return trimmed; -} - -export async function sendImessage(toNumber: string, text: string): Promise { - const h = headers(); - if (!h) { - console.warn("[sendblue] missing credentials — not sending"); - return; - } - const from = normalizeE164(process.env.SENDBLUE_FROM_NUMBER); - if (!from) { - console.error( - `[sendblue] SENDBLUE_FROM_NUMBER is not set. Run \`npm run sendblue:sync\` (pulls it from \`sendblue lines\`) or paste your provisioned number into .env.local, then restart \`npm run dev\`.`, - ); - return; - } - const plain = stripMarkdown(text); - for (const part of chunk(plain)) { - const res = await fetch(`${API_BASE}/send-message`, { - method: "POST", - headers: h, - body: JSON.stringify({ number: toNumber, content: part, from_number: from }), - }); - if (!res.ok) { - const body = await res.text().catch(() => ""); - console.error(`[sendblue] send failed ${res.status}: ${body}`); - if (body.includes("missing required parameter") && body.includes("from_number")) { - console.error( - `[sendblue] → Set SENDBLUE_FROM_NUMBER in .env.local to your Sendblue-provisioned number and restart the server.`, - ); - } else if (body.includes("Cannot send messages to self")) { - console.error( - `[sendblue] → SENDBLUE_FROM_NUMBER is your personal cell. It must be the Sendblue-provisioned number (the one people text TO).`, - ); - } else if (body.includes("This phone number is not defined")) { - console.error( - `[sendblue] → Sendblue doesn't recognize from_number=${from}. Run \`npm run sendblue:sync\` to pull the correct one from \`sendblue lines\`, then restart the server.`, - ); - } - } else { - console.log(`[sendblue] → sent ${part.length} chars to ${toNumber}`); - } - } -} - -export async function sendTypingIndicator(toNumber: string): Promise { - const h = headers(); - if (!h) return; - const from = process.env.SENDBLUE_FROM_NUMBER; - try { - await fetch(`${API_BASE}/send-typing-indicator`, { - method: "POST", - headers: h, - body: JSON.stringify({ number: toNumber, from_number: from }), - }); - } catch { - /* non-fatal */ - } -} - -export function startTypingLoop(toNumber: string): () => void { - sendTypingIndicator(toNumber); - const timer = setInterval(() => sendTypingIndicator(toNumber), 5000); - return () => clearInterval(timer); -} - -export function createSendblueRouter(): express.Router { - const router = express.Router(); - - router.post("/webhook", async (req, res) => { - const { content, from_number, is_outbound, message_handle } = req.body ?? {}; - if (is_outbound || !content || !from_number) { - res.json({ ok: true, skipped: true }); - return; - } - - if (message_handle) { - const { claimed } = await convex.mutation(api.sendblueDedup.claim, { - handle: message_handle, - }); - if (!claimed) { - res.json({ ok: true, deduped: true }); - return; - } - } - - const conversationId = `sms:${from_number}`; - const turnTag = Math.random().toString(36).slice(2, 8); - const preview = content.length > 100 ? content.slice(0, 100) + "…" : content; - console.log(`[turn ${turnTag}] ← ${from_number}: ${JSON.stringify(preview)}`); - const start = Date.now(); - - broadcast("message_in", { conversationId, content, from_number, handle: message_handle }); - res.json({ ok: true }); - - const stopTyping = startTypingLoop(from_number); - try { - const reply = await handleUserMessage({ - conversationId, - content, - turnTag, - onThinking: (t) => broadcast("thinking", { conversationId, t }), - }); - if (reply) { - const elapsed = ((Date.now() - start) / 1000).toFixed(1); - const replyPreview = reply.length > 100 ? reply.slice(0, 100) + "…" : reply; - console.log( - `[turn ${turnTag}] → reply (${elapsed}s, ${reply.length} chars): ${JSON.stringify(replyPreview)}`, - ); - await sendImessage(from_number, reply); - await convex.mutation(api.messages.send, { - conversationId, - role: "assistant", - content: reply, - }); - } else { - console.log(`[turn ${turnTag}] → (no reply)`); - } - } catch (err) { - console.error(`[turn ${turnTag}] handler error`, err); - } finally { - stopTyping(); - } - }); - - return router; -} diff --git a/skills-lock.json b/skills-lock.json index bf33b964..4fec58b3 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,35 +1,40 @@ { "version": 1, "skills": { + "chat-sdk": { + "source": "vercel/chat", + "sourceType": "github", + "computedHash": "eac44bca0c67339cb4abf9c3c9252e1888f42cb9c8d2d8bc5d3c7540641fc93d" + }, "convex": { "source": "get-convex/agent-skills", "sourceType": "github", - "computedHash": "613ee9955985085d0fca8f96e1fc6d7cfd204dffa203499a1d508b8def76577b" + "computedHash": "70ecfb9cd4439ccbf6570b6dc23eab53f7ce7dcf70ef63bbfdf8f4f21353dfb4" }, "convex-create-component": { "source": "get-convex/agent-skills", "sourceType": "github", - "computedHash": "d110fca7f65b4919367e6fc63a93bf54abea2cf5e4e097234c947559ffa6e527" + "computedHash": "e4ad9cbe6d2bb0d5171dfd04019bc4ff228f26fb52312429376c885d2ec4935a" }, "convex-migration-helper": { "source": "get-convex/agent-skills", "sourceType": "github", - "computedHash": "46d1ac354eefbed05e1367d828e893816c13302276080bfaf6bcd828281be486" + "computedHash": "c6416032d2f2e947ebe9d6b2389d89592d0229a0e6c4202f9a1197f2bd76019f" }, "convex-performance-audit": { "source": "get-convex/agent-skills", "sourceType": "github", - "computedHash": "30ea0d3c259df011e44ea9b70502ab272f5ac3bd1fb3672ae18489ba99b2c4ae" + "computedHash": "c048b44beca5616108bfebc9822b6238cbff5c99facb88b3cf3d3a2af0dac502" }, "convex-quickstart": { "source": "get-convex/agent-skills", "sourceType": "github", - "computedHash": "8ae9e1b02f526ea65e7895fac82af74142cd8e70e364d9dae9dbf79a296fb5ef" + "computedHash": "c95728c430a441325c865b06f0f0e912923c34deecbf6f24e9f03e13046b469c" }, "convex-setup-auth": { "source": "get-convex/agent-skills", "sourceType": "github", - "computedHash": "e719d31d1ab0d19ca7b942d1154d3ff436b5c156900eea9866c2aaeb910a1388" + "computedHash": "f60559165edd5b616fda726ed5726c798e33f905361ed9892ab6013e53ab2588" } } }