From 77adaf3d96bd839a49aa88afaee100fc6b8a6754 Mon Sep 17 00:00:00 2001 From: nave Date: Mon, 27 Apr 2026 19:40:10 +0800 Subject: [PATCH] feat: add Lark channel adapter Adds a built-in Lark/Feishu adapter with setup wizard support, documentation, and tests so users can connect OpenACP through a self-built Lark bot over WebSocket. Also improves Windows package-runner resolution and makes the build script cross-platform. --- CLAUDE.md | 5 + README.md | 1 + docs/gitbook/SUMMARY.md | 1 + docs/gitbook/platform-setup/README.md | 1 + docs/gitbook/platform-setup/lark.md | 110 ++ package.json | 3 +- pnpm-lock.yaml | 413 +++++++ scripts/build.ts | 31 + src/core/agents/agent-instance.ts | 76 +- src/core/setup/__tests__/helpers.test.ts | 13 + .../setup/__tests__/setup-channels.test.ts | 29 + src/core/setup/helpers.ts | 1 + src/core/setup/setup-channels.ts | 15 +- src/core/setup/types.ts | 1 + src/core/setup/wizard.ts | 18 + src/plugins/__tests__/plugin-wrappers.test.ts | 7 +- src/plugins/core-plugins.ts | 4 +- src/plugins/index.ts | 2 + src/plugins/lark/__tests__/formatting.test.ts | 238 ++++ .../lark/__tests__/permissions.test.ts | 200 ++++ .../lark/__tests__/thread-manager.test.ts | 160 +++ src/plugins/lark/adapter.ts | 1007 +++++++++++++++++ src/plugins/lark/formatting.ts | 227 ++++ src/plugins/lark/index.ts | 243 ++++ src/plugins/lark/lark-client.ts | 625 ++++++++++ src/plugins/lark/permissions.ts | 135 +++ src/plugins/lark/renderer.ts | 119 ++ src/plugins/lark/thread-manager.ts | 132 +++ src/plugins/lark/types.ts | 87 ++ src/plugins/lark/validators.ts | 158 +++ 30 files changed, 4027 insertions(+), 35 deletions(-) create mode 100644 docs/gitbook/platform-setup/lark.md create mode 100644 scripts/build.ts create mode 100644 src/plugins/lark/__tests__/formatting.test.ts create mode 100644 src/plugins/lark/__tests__/permissions.test.ts create mode 100644 src/plugins/lark/__tests__/thread-manager.test.ts create mode 100644 src/plugins/lark/adapter.ts create mode 100644 src/plugins/lark/formatting.ts create mode 100644 src/plugins/lark/index.ts create mode 100644 src/plugins/lark/lark-client.ts create mode 100644 src/plugins/lark/permissions.ts create mode 100644 src/plugins/lark/renderer.ts create mode 100644 src/plugins/lark/thread-manager.ts create mode 100644 src/plugins/lark/types.ts create mode 100644 src/plugins/lark/validators.ts diff --git a/CLAUDE.md b/CLAUDE.md index adbd2bfa..85762cff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,7 @@ src/ plugins/ — All plugins (adapters + services) telegram/ — Telegram adapter (grammY) slack/ — Slack adapter (@slack/bolt) + lark/ — Lark / Feishu adapter (REST + WebSocket event stream) speech/ — TTS/STT (Edge TTS, Groq STT) tunnel/ — Port forwarding (Cloudflare, ngrok, Bore, Tailscale) security/ — Access control, rate limiting @@ -227,3 +228,7 @@ Users who installed and ran older versions will have config, data, and storage i - **CLI flags & commands**: Do not remove or rename existing commands/flags. If deprecating, keep them working and log a warning. - **Plugin API**: When changing interfaces that plugins use, must maintain backward compat or bump major version. - **General rule**: New code must work with old data/config without requiring user action. If migration is needed, run it automatically on startup. + +## Local OpenACP Workspace + +The `.openacp/` directory contains a local OpenACP workspace with secrets (bot tokens, API keys). Do not read, commit, or reference files inside it. diff --git a/README.md b/README.md index 8b53ada3..9ba57f86 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ That's it. Send a message to your bot and start coding. | **Telegram** | Stable | Forum topics per session, streaming, permission buttons, voice | | **Discord** | Stable | Thread-based sessions, slash commands, button interactions | | **Slack** | Stable | Socket Mode, channel-based sessions, thread organization | +| **Lark / Feishu** | Beta | Single-chat + thread-per-session model, interactive cards, WebSocket event stream, voice | ### Core diff --git a/docs/gitbook/SUMMARY.md b/docs/gitbook/SUMMARY.md index 175f9f42..8dde4231 100644 --- a/docs/gitbook/SUMMARY.md +++ b/docs/gitbook/SUMMARY.md @@ -13,6 +13,7 @@ * [Telegram](platform-setup/telegram.md) * [Discord](platform-setup/discord.md) * [Slack](platform-setup/slack.md) +* [Lark / Feishu](platform-setup/lark.md) ## Using OpenACP diff --git a/docs/gitbook/platform-setup/README.md b/docs/gitbook/platform-setup/README.md index 463dcb65..3fd1dd0b 100644 --- a/docs/gitbook/platform-setup/README.md +++ b/docs/gitbook/platform-setup/README.md @@ -9,6 +9,7 @@ OpenACP connects to your team's messaging platform and routes messages to AI cod | [Telegram](telegram.md) | Personal use, solo developers, mobile-first workflows | ~5 minutes | | [Discord](discord.md) | Developer teams with existing Discord servers | ~10 minutes | | [Slack](slack.md) | Enterprise teams, organizations on Slack | ~15 minutes | +| [Lark / Feishu](lark.md) | Teams using Feishu (China) or Lark (international) | ~10 minutes | Each platform requires you to create a bot or app in that platform's developer portal, then paste the credentials into `/.openacp/config.json`. The interactive setup wizard (`openacp`) guides you through the process step by step. diff --git a/docs/gitbook/platform-setup/lark.md b/docs/gitbook/platform-setup/lark.md new file mode 100644 index 00000000..a6477399 --- /dev/null +++ b/docs/gitbook/platform-setup/lark.md @@ -0,0 +1,110 @@ +# Lark / Feishu Setup + +OpenACP supports both Feishu (the Chinese deployment, `open.feishu.cn`) and Lark (the international deployment, `open.larksuite.com`). The adapter uses a single group chat where each session lives under its own message thread, and connects to Lark's event API over a long-lived WebSocket — no public webhook URL or tunnel is required. + +## Prerequisites + +- A Lark or Feishu account with admin rights to create custom apps in the developer console. +- A Lark/Feishu group chat that the bot will join. Sessions are scoped to this single chat. +- Node.js 20+ on the machine running OpenACP. + +## Step 1 — Create the Lark Open Platform app + +1. Visit the developer console: + - Feishu (China): [https://open.feishu.cn/app](https://open.feishu.cn/app) + - Lark (international): [https://open.larksuite.com/app](https://open.larksuite.com/app) +2. Click **Create Custom App**, give it a name (e.g. "OpenACP"), and pick an icon. +3. Open the new app and enable the **Bot** capability under "Add Features". +4. Under **Permissions & Scopes**, grant the following scopes: + - `im:message` — receive messages + - `im:message:send_as_bot` — send messages as the bot + - `im:chat:readonly` — read group chat metadata (required for the install validator) + - `im:resource` — download images, audio, and files attached to messages + - `im:message.group_at_msg` — receive `@bot` mentions in group chats +5. Apply for version review if your tenant requires it (most internal apps can skip this step). +6. Copy your `App ID` (starts with `cli_…`) and `App Secret` from the **Credentials & Basic Info** page. + +## Step 2 — Add the bot to your group + +1. Open the target group chat in Lark/Feishu. +2. Tap the chat name → **Group Settings** → **Group Bot** → **Add Bot**. +3. Search for your custom app and add it. +4. Copy the chat ID. The easiest way is via the Lark API explorer: + - In the developer console, open **API Explorer** → `im/v1/chats` → call `GET /open-apis/im/v1/chats` with the bot's tenant token. + - The response includes a list of chats the bot is a member of, each with its `chat_id` (starts with `oc_…`). + +## Step 3 — Run the OpenACP setup wizard + +```bash +npx @openacp/cli plugins install @openacp/lark +``` + +This is the same wizard the Telegram and Slack adapters use. You will be prompted for: + +| Prompt | What to enter | +|--------|---------------| +| Domain | `feishu` for Chinese deployments, `lark` for international | +| App ID | The `cli_…` ID from the developer console | +| App Secret | The matching secret (input is masked) | +| Group chat ID | The `oc_…` ID from Step 2 | + +The wizard validates each value before saving: + +- The credentials are exchanged for a tenant access token. +- The token is used to fetch the chat info — confirming the bot has joined. + +If validation fails, you can choose to retry or skip and fix the issue later. Saved settings live at `/.openacp/plugins/@openacp/lark/settings.json`. + +## Step 4 — First boot + +Start OpenACP normally: + +```bash +npx @openacp/cli start +``` + +On first boot the adapter creates two anchor messages in your group: + +- 📋 **Notifications** — cross-session updates (session completed, permission needed, budget warnings). +- 🤖 **Assistant** — the OpenACP conversational entry point. Reply to this message to chat with the assistant. + +Both anchor message IDs are persisted to plugin settings so they are reused on every restart. + +To start a coding session, create one through the **Assistant** thread (e.g. "create a session for the foo project") or via the API/CLI; OpenACP posts a new session anchor in the chat and threads agent activity under it. + +## How sessions map to Lark threads + +Lark does not have first-class "topics" like Telegram, but it does support threaded replies via the `reply_in_thread` flag. OpenACP uses this to give every session its own collapsible thread: + +``` +Group chat +├── 📋 Notifications (root message — stays at the top) +│ ├── ✅ session-1 completed +│ └── 🔐 session-2 needs permission +├── 🤖 Assistant (root message) +│ └── (assistant chat) +├── 🟢 session-1 (session anchor card) +│ ├── user prompt +│ ├── 🔄 Read · src/foo.ts +│ └── ✅ Done +└── 🟢 session-2 + └── … +``` + +When you reply to a session anchor, OpenACP looks up the session by the root message ID and forwards your message to the agent. Direct top-level messages (not in a thread) are gently redirected to the **Assistant** thread. + +## Permissions, control panels, and command buttons + +Rich UI is rendered as Lark interactive cards (`update_multi: true`) so the adapter can edit them in place: + +- **Permission requests** appear as orange-templated cards with one button per option. Clicks come back over the WebSocket and resolve the agent's blocked prompt. +- **Tool activity** is shown as cards summarizing reads, edits, and shell commands, with optional "View file" / "View diff" links if the [tunnel plugin](../features/tunnel.md) is enabled. +- **Slash commands** (`/help`, `/agents`, etc.) are dispatched through the same `CommandRegistry` used by the other adapters; results are rendered as cards or plain text replies inside the originating thread. + +## Limitations & roadmap + +- HTTP webhook callbacks are not implemented yet — the adapter currently uses Lark's WebSocket event stream exclusively. (Plugin settings retain encrypt/verification token fields so a future release can switch transports without re-running setup.) +- Skill commands (per-session interactive command lists shown in the Telegram bot menu) are not yet surfaced in the Lark UI. They are accepted by the adapter but rendered as no-ops. +- Renaming a session changes the anchor card; Lark does not allow renaming threads themselves. + +If you hit issues, see [Lark issues](../troubleshooting/lark-issues.md) or open a discussion at [github.com/Open-ACP/OpenACP](https://github.com/Open-ACP/OpenACP/discussions). diff --git a/package.json b/package.json index 2d6e79a4..2a1200a2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "openacp": "dist/cli.js" }, "scripts": { - "build": "tsc && mkdir -p dist/data && cp src/data/registry-snapshot.json dist/data/", + "build": "tsx scripts/build.ts", "build:publish": "tsx scripts/build-publish.ts", "dev": "./scripts/dev-loop.sh", "dev:loop": "./scripts/dev-loop.sh", @@ -46,6 +46,7 @@ "@fastify/rate-limit": "^10.3.0", "@fastify/swagger": "^9.7.0", "@fastify/swagger-ui": "^5.2.5", + "@larksuiteoapi/node-sdk": "^1.62.0", "diff": "^8.0.3", "fastest-levenshtein": "^1.0.16", "fastify": "^5.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbc79dc9..c59b551b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@fastify/swagger-ui': specifier: ^5.2.5 version: 5.2.5 + '@larksuiteoapi/node-sdk': + specifier: ^1.62.0 + version: 1.62.0 diff: specifier: ^8.0.3 version: 8.0.3 @@ -391,56 +394,66 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} @@ -467,6 +480,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@larksuiteoapi/node-sdk@1.62.0': + resolution: {integrity: sha512-ZITiuAkiVgphn6OPO8MHeWV1q7+UNByLmNiYVDIAxF5+HJ8USl4xPinDOq9AMJSEUqdBJtiLdz7UltV5jP+EDg==} + '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -480,6 +496,36 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rolldown/binding-android-arm64@1.0.0-rc.10': resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -515,36 +561,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} @@ -606,66 +658,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -835,6 +900,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -842,6 +910,9 @@ packages: avvio@9.2.0: resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -867,6 +938,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -901,6 +980,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -946,6 +1029,10 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -975,6 +1062,10 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -989,12 +1080,28 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.27.4: resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} @@ -1080,15 +1187,39 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} @@ -1096,10 +1227,26 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + grammy@1.41.1: resolution: {integrity: sha512-wcHAQ1e7svL3fJMpDchcQVcWUmywhuepOOjHUHmMmWAwUJEIyK5ea5sbSjZd+Gy1aMpZeP8VYJa+4tP+j1YptQ==} engines: {node: ^12.20.0 || >=14.13.1} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -1207,24 +1354,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1253,6 +1404,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash.identity@3.0.0: + resolution: {integrity: sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -1271,13 +1425,22 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.pickby@4.6.0: + resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} + log-symbols@7.0.1: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -1288,10 +1451,22 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1348,6 +1523,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1452,6 +1631,13 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + protobufjs@7.5.5: + resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} + engines: {node: '>=12.0.0'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -1459,6 +1645,10 @@ packages: resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} hasBin: true + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -1534,6 +1724,22 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1865,6 +2071,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -2161,6 +2379,20 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@larksuiteoapi/node-sdk@1.62.0': + dependencies: + axios: 1.13.6 + lodash.identity: 3.0.0 + lodash.merge: 4.6.2 + lodash.pickby: 4.6.0 + protobufjs: 7.5.5 + qs: 6.15.1 + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + '@lukeed/ms@2.0.2': {} '@napi-rs/wasm-runtime@1.1.1': @@ -2174,6 +2406,29 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@rolldown/binding-android-arm64@1.0.0-rc.10': optional: true @@ -2457,6 +2712,8 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} avvio@9.2.0: @@ -2464,6 +2721,14 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.20.1 + axios@1.13.6: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@4.0.4: {} brace-expansion@5.0.5: @@ -2483,6 +2748,16 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2511,6 +2786,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} confbox@0.1.8: {} @@ -2535,6 +2814,8 @@ snapshots: deepmerge@4.3.1: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -2561,6 +2842,12 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -2573,10 +2860,25 @@ snapshots: entities@7.0.1: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + esbuild@0.27.4: optionalDependencies: '@esbuild/aix-ppc64': 0.27.4 @@ -2695,11 +2997,41 @@ snapshots: mlly: 1.8.1 rollup: 4.59.0 + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -2710,6 +3042,8 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + gopd@1.2.0: {} + grammy@1.41.1: dependencies: '@grammyjs/types': 3.25.0 @@ -2720,6 +3054,16 @@ snapshots: - encoding - supports-color + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + help-me@5.0.0: {} htmlparser2@10.1.0: @@ -2856,6 +3200,8 @@ snapshots: load-tsconfig@0.2.5: {} + lodash.identity@3.0.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -2868,13 +3214,19 @@ snapshots: lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.pickby@4.6.0: {} + log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 yoctocolors: 2.1.2 + long@5.3.2: {} + loupe@3.2.1: {} lru-cache@11.2.7: {} @@ -2883,11 +3235,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime@3.0.0: {} mimic-function@5.0.1: {} @@ -2927,6 +3287,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + obug@2.1.1: {} ogg-opus-decoder@1.7.3: @@ -3047,6 +3409,23 @@ snapshots: process-warning@5.0.0: {} + protobufjs@7.5.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.5.0 + long: 5.3.2 + + proxy-from-env@1.1.0: {} + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -3054,6 +3433,10 @@ snapshots: qrcode-terminal@0.12.0: {} + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + quick-format-unescaped@4.0.4: {} readdirp@4.1.2: {} @@ -3154,6 +3537,34 @@ snapshots: setprototypeof@1.2.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -3428,6 +3839,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.20.0: {} + yaml@2.8.3: {} yoctocolors@2.1.2: {} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 00000000..1a82ecbf --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,31 @@ +import { mkdir, copyFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawn } from 'node:child_process' + +const root = dirname(dirname(fileURLToPath(import.meta.url))) + +async function run(command: string, args: string[]): Promise { + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: root, + stdio: 'inherit', + shell: process.platform === 'win32', + }) + child.on('error', reject) + child.on('exit', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 'unknown'}`)) + } + }) + }) +} + +await run('tsc', []) +await mkdir(join(root, 'dist', 'data'), { recursive: true }) +await copyFile( + join(root, 'src', 'data', 'registry-snapshot.json'), + join(root, 'dist', 'data', 'registry-snapshot.json'), +) diff --git a/src/core/agents/agent-instance.ts b/src/core/agents/agent-instance.ts index 1928db9c..8e349f09 100644 --- a/src/core/agents/agent-instance.ts +++ b/src/core/agents/agent-instance.ts @@ -72,6 +72,14 @@ function findPackageRoot(startDir: string): string { return startDir; } +function commandForWindowsScript(filePath: string): { command: string; args: string[] } { + const ext = path.extname(filePath).toLowerCase(); + if (process.platform === "win32" && (ext === ".cmd" || ext === ".bat")) { + return { command: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", `"${filePath}"`] }; + } + return { command: filePath, args: [] }; +} + /** * Resolve an agent command name to a directly executable form. * @@ -130,32 +138,13 @@ function resolveAgentCommand(cmd: string): { command: string; args: string[] } { } } - // 3. Try resolving from PATH using which - try { - const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim(); - if (fullPath) { - try { - const content = fs.readFileSync(fullPath, "utf-8"); - if (content.startsWith("#!/usr/bin/env node")) { - return { command: process.execPath, args: [fullPath] }; - } - } catch { - // Binary file (not readable as utf-8) — use full path directly - } - // Found via PATH but not a node script — use the resolved full path - return { command: fullPath, args: [] }; - } - } catch { - // which failed — command not on PATH - } - - // 4. For npx/uvx: derive from the running Node's bin directory. + // 3. For npx/uvx: derive from the running Node's bin directory. // When openacp is installed globally (e.g. via Homebrew or nvm), npx lives // next to the same node binary that is executing this process. The user's // shell PATH may not include that directory (common with nvm in non-interactive - // shells), so resolve it explicitly. + // shells), so resolve it explicitly. On Windows, prefer .cmd/.exe wrappers; + // extensionless shim files cannot be spawned reliably by CreateProcess. if (cmd === "npx" || cmd === "uvx") { - // Collect candidate directories: process.execPath, its realpath, and well-known locations const seen = new Set(); const candidates: string[] = []; const addCandidate = (dir: string) => { @@ -164,18 +153,49 @@ function resolveAgentCommand(cmd: string): { command: string; args: string[] } { addCandidate(path.dirname(process.execPath)); try { addCandidate(path.dirname(fs.realpathSync(process.execPath))); } catch { /* ignore */ } - // Well-known Node.js install locations on macOS/Linux addCandidate("/opt/homebrew/bin"); addCandidate("/usr/local/bin"); + const executableNames = process.platform === "win32" ? [`${cmd}.cmd`, `${cmd}.exe`, cmd] : [cmd]; for (const dir of candidates) { - const candidate = path.join(dir, cmd); - if (fs.existsSync(candidate)) { - log.info({ cmd, resolved: candidate }, "Resolved package runner from fallback search"); - return { command: candidate, args: [] }; + if (cmd === "npx") { + const npxCli = path.join(dir, "node_modules", "npm", "bin", "npx-cli.js"); + if (fs.existsSync(npxCli)) { + log.info({ cmd, resolved: npxCli }, "Resolved npx CLI from Node installation"); + return { command: process.execPath, args: [npxCli] }; + } } + for (const name of executableNames) { + const candidate = path.join(dir, name); + if (fs.existsSync(candidate)) { + const resolved = commandForWindowsScript(candidate); + log.info({ cmd, resolved: candidate }, "Resolved package runner from fallback search"); + return resolved; + } + } + } + } + + // 4. Try resolving from PATH using which + try { + const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim(); + if (fullPath) { + if (process.platform === "win32" && (cmd === "npx" || cmd === "uvx") && !path.extname(fullPath)) { + throw new Error("Skipping extensionless Windows package runner"); + } + try { + const content = fs.readFileSync(fullPath, "utf-8"); + if (content.startsWith("#!/usr/bin/env node")) { + return { command: process.execPath, args: [fullPath] }; + } + } catch { + // Binary file (not readable as utf-8) — use full path directly + } + // Found via PATH but not a node script — use the resolved full path + return commandForWindowsScript(fullPath); } - log.warn({ cmd, execPath: process.execPath, candidates }, "Could not find package runner"); + } catch { + // which failed — command not on PATH } // 5. Fallback: use command as-is diff --git a/src/core/setup/__tests__/helpers.test.ts b/src/core/setup/__tests__/helpers.test.ts index 1a602f2c..21cfeaf5 100644 --- a/src/core/setup/__tests__/helpers.test.ts +++ b/src/core/setup/__tests__/helpers.test.ts @@ -59,4 +59,17 @@ describe('summarizeConfig', () => { expect(summary).toContain('Telegram (not configured)') }) + it('shows Lark / Feishu as enabled when plugin settings have app credentials and chatId', async () => { + await settingsManager.updatePluginSettings('@openacp/lark', { + appId: 'cli_test', + appSecret: 'secret', + chatId: 'oc_test', + }) + + const config = makeEmptyConfig() + const summary = await summarizeConfig(config, settingsManager) + + expect(summary).toContain('Lark / Feishu (enabled)') + }) + }) diff --git a/src/core/setup/__tests__/setup-channels.test.ts b/src/core/setup/__tests__/setup-channels.test.ts index 8a4d667b..180130f7 100644 --- a/src/core/setup/__tests__/setup-channels.test.ts +++ b/src/core/setup/__tests__/setup-channels.test.ts @@ -76,4 +76,33 @@ describe('getChannelStatuses', () => { const tg = statuses.find(s => s.id === 'telegram') expect(tg?.configured).toBe(true) }) + + it('shows lark as configured when plugin settings have app credentials and chatId', async () => { + await settingsManager.updatePluginSettings('@openacp/lark', { + appId: 'cli_test', + appSecret: 'secret', + chatId: 'oc_test', + }) + + const config = makeEmptyConfig() + const statuses = await getChannelStatuses(config, settingsManager) + + const lark = statuses.find(s => s.id === 'lark') + expect(lark?.configured).toBe(true) + expect(lark?.enabled).toBe(true) + expect(lark?.hint).toBe('Chat ID: oc_test') + }) + + it('shows lark as not configured when appSecret is missing', async () => { + await settingsManager.updatePluginSettings('@openacp/lark', { + appId: 'cli_test', + chatId: 'oc_test', + }) + + const config = makeEmptyConfig() + const statuses = await getChannelStatuses(config, settingsManager) + + const lark = statuses.find(s => s.id === 'lark') + expect(lark?.configured).toBe(false) + }) }) diff --git a/src/core/setup/helpers.ts b/src/core/setup/helpers.ts index fa0681b2..6c086b3f 100644 --- a/src/core/setup/helpers.ts +++ b/src/core/setup/helpers.ts @@ -93,6 +93,7 @@ export async function summarizeConfig(config: Config, settingsManager?: Settings // Channels — check plugin settings (new-style) before falling back to config.channels (legacy) const channelDefs: Array<{ id: string; label: string; pluginName: string; keys: string[] }> = [ { id: "telegram", label: "Telegram", pluginName: "@openacp/telegram", keys: ["botToken", "chatId"] }, + { id: "lark", label: "Lark / Feishu", pluginName: "@openacp/lark", keys: ["appId", "appSecret", "chatId"] }, { id: "discord", label: "Discord", pluginName: "@openacp/discord-adapter", keys: ["guildId", "token"] }, ]; diff --git a/src/core/setup/setup-channels.ts b/src/core/setup/setup-channels.ts index 3b8272de..d1c59136 100644 --- a/src/core/setup/setup-channels.ts +++ b/src/core/setup/setup-channels.ts @@ -1,6 +1,6 @@ /** * Channel configuration step — manages messaging platform setup - * (Telegram, Discord, Desktop App) via plugin install/configure hooks. + * (Telegram, Lark, Discord, Desktop App) via plugin install/configure hooks. */ import * as clack from "@clack/prompts"; @@ -11,8 +11,9 @@ import { CHANNEL_META } from "./types.js"; import { guardCancel, ok, c } from "./helpers.js"; // Maps logical channel ID → plugin name used for settings storage and dynamic import. -// Telegram is built-in so it uses a direct import path instead of this map. +// Telegram and Lark are built-in so they use direct import paths instead of this map. const CHANNEL_PLUGIN_NAME: Record = { + lark: "@openacp/lark", discord: "@openacp/discord-adapter", }; @@ -36,6 +37,13 @@ export async function getChannelStatuses(config: Config, settingsManager?: Setti enabled = ps.enabled !== false; // enabled by default when configured hint = `Chat ID: ${ps.chatId}`; } + } else if (settingsManager && id === "lark") { + const ps = await settingsManager.loadSettings("@openacp/lark"); + if (ps.appId && ps.appSecret && ps.chatId) { + configured = true; + enabled = ps.enabled !== false; + hint = `Chat ID: ${ps.chatId}`; + } } else if (settingsManager && id === "discord") { const ps = await settingsManager.loadSettings("@openacp/discord-adapter"); if (ps.guildId || ps.token) { @@ -95,6 +103,9 @@ async function configureViaPlugin(channelId: string, isConfigured: boolean, sett if (channelId === 'telegram') { const pluginModule = await import('../../plugins/telegram/index.js'); plugin = pluginModule.default; + } else if (channelId === 'lark') { + const pluginModule = await import('../../plugins/lark/index.js'); + plugin = pluginModule.default; } else { // Use the known plugin package name; fall back to scoped package name for unknown adapters. const packageName = CHANNEL_PLUGIN_NAME[channelId] ?? `@openacp/${channelId}`; diff --git a/src/core/setup/types.ts b/src/core/setup/types.ts index df52bac5..38daddaa 100644 --- a/src/core/setup/types.ts +++ b/src/core/setup/types.ts @@ -45,6 +45,7 @@ export const ONBOARD_SECTION_OPTIONS: Array<{ export const CHANNEL_META: Record = { sse: { label: "Desktop App", method: "SSE" }, telegram: { label: "Telegram", method: "Bot API" }, + lark: { label: "Lark / Feishu", method: "WebSocket" }, discord: { label: "Discord", method: "Bot API" }, }; diff --git a/src/core/setup/wizard.ts b/src/core/setup/wizard.ts index 669b02fc..ea47b743 100644 --- a/src/core/setup/wizard.ts +++ b/src/core/setup/wizard.ts @@ -228,6 +228,7 @@ export async function runSetup( const builtInOptions = [ { label: 'Desktop App', value: 'sse' }, { label: 'Telegram', value: 'telegram' }, + { label: 'Lark / Feishu', value: 'lark' }, ] const officialAdapters = [ @@ -309,6 +310,23 @@ export async function runSetup( }); } + if (channelId === 'lark') { + const larkPlugin = (await import('../../plugins/lark/index.js')).default; + const ctx = createInstallContext({ + pluginName: larkPlugin.name, + settingsManager, + basePath: settingsManager.getBasePath(), + }); + await larkPlugin.install!(ctx); + pluginRegistry.register(larkPlugin.name, { + version: larkPlugin.version, + source: 'builtin', + enabled: true, + settingsPath: settingsManager.getSettingsPath(larkPlugin.name), + description: larkPlugin.description, + }); + } + // Handle official adapter selections (Discord, Slack, etc.) if (channelId.startsWith('official:')) { diff --git a/src/plugins/__tests__/plugin-wrappers.test.ts b/src/plugins/__tests__/plugin-wrappers.test.ts index 82c5aadf..aba2a0ad 100644 --- a/src/plugins/__tests__/plugin-wrappers.test.ts +++ b/src/plugins/__tests__/plugin-wrappers.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest' import { builtInPlugins } from '../index.js' describe('Built-in plugin wrappers', () => { - it('exports all 8 built-in plugins', () => { - expect(builtInPlugins).toHaveLength(8) + it('exports all 9 built-in plugins', () => { + expect(builtInPlugins).toHaveLength(9) }) it('all plugins have name, version, setup', () => { @@ -31,11 +31,12 @@ describe('Built-in plugin wrappers', () => { expect(names).toContain('@openacp/tunnel') expect(names).toContain('@openacp/api-server') expect(names).toContain('@openacp/telegram') + expect(names).toContain('@openacp/lark') }) it('adapter plugins depend on security and notifications', () => { const adapters = builtInPlugins.filter(p => - ['@openacp/telegram'].includes(p.name) + ['@openacp/telegram', '@openacp/lark'].includes(p.name) ) for (const adapter of adapters) { expect(adapter.pluginDependencies).toBeDefined() diff --git a/src/plugins/core-plugins.ts b/src/plugins/core-plugins.ts index 9199f945..44959f0f 100644 --- a/src/plugins/core-plugins.ts +++ b/src/plugins/core-plugins.ts @@ -13,6 +13,7 @@ import tunnelPlugin from './tunnel/index.js' import apiServerPlugin from './api-server/index.js' import sseAdapterPlugin from './sse-adapter/index.js' import telegramPlugin from './telegram/index.js' +import larkPlugin from './lark/index.js' /** * Ordered list of all bundled plugins, passed to `LifecycleManager.boot()` on startup. @@ -25,7 +26,7 @@ import telegramPlugin from './telegram/index.js' * 1. **Service plugins** — security, file-service, context, speech, notifications. * These provide services that infrastructure and adapter plugins depend on. * 2. **Infrastructure plugins** — tunnel (exposes the local server), api-server (HTTP + SSE). - * 3. **Adapter plugins** — sse-adapter, telegram. Both depend on api-server, security, and + * 3. **Adapter plugins** — sse-adapter, telegram, lark. All depend on api-server, security, and * notifications, so they must boot after those services are ready. */ export const corePlugins = [ @@ -46,4 +47,5 @@ export const corePlugins = [ // Adapter plugins (depend on security, notifications, etc.) sseAdapterPlugin, telegramPlugin, + larkPlugin, ] diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 17643aa6..984bb593 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -8,6 +8,7 @@ import notificationsPlugin from './notifications/index.js' import tunnelPlugin from './tunnel/index.js' import apiServerPlugin from './api-server/index.js' import telegramPlugin from './telegram/index.js' +import larkPlugin from './lark/index.js' export const builtInPlugins = [ securityPlugin, @@ -18,4 +19,5 @@ export const builtInPlugins = [ tunnelPlugin, apiServerPlugin, telegramPlugin, + larkPlugin, ] diff --git a/src/plugins/lark/__tests__/formatting.test.ts b/src/plugins/lark/__tests__/formatting.test.ts new file mode 100644 index 00000000..fe06a2a0 --- /dev/null +++ b/src/plugins/lark/__tests__/formatting.test.ts @@ -0,0 +1,238 @@ +import { describe, it, expect } from "vitest"; +import { + escapeLarkMd, + buildTextContent, + buildMarkdownCard, + buildActionButtonsCard, + formatToolCall, + formatToolUpdate, + formatPlan, + formatUsage, + splitMessage, +} from "../formatting.js"; +import type { ToolCallMeta, ToolUpdateMeta } from "../../../core/adapter-primitives/format-types.js"; + +describe("Lark formatting helpers", () => { + describe("escapeLarkMd", () => { + it("encodes <, >, and & to keep them literal in lark_md", () => { + expect(escapeLarkMd("a < b > c & d")).toBe("a < b > c & d"); + }); + + it("returns empty string for null / undefined / empty input", () => { + expect(escapeLarkMd(null)).toBe(""); + expect(escapeLarkMd(undefined)).toBe(""); + expect(escapeLarkMd("")).toBe(""); + }); + + it("preserves text without special characters unchanged", () => { + expect(escapeLarkMd("hello world")).toBe("hello world"); + }); + + it("handles repeated special characters", () => { + expect(escapeLarkMd("&&&<<<>>>")).toBe("&&&<<<>>>"); + }); + }); + + describe("buildTextContent", () => { + it("produces JSON-encoded {text} payload as required by Lark API", () => { + expect(buildTextContent("hello")).toBe('{"text":"hello"}'); + }); + + it("escapes special characters in JSON encoding", () => { + // Lark requires the content field to be a JSON-encoded string. JSON.stringify + // already escapes quotes; we just verify the structure is preserved. + const content = buildTextContent('he said "hi"'); + const parsed = JSON.parse(content); + expect(parsed.text).toBe('he said "hi"'); + }); + }); + + describe("buildMarkdownCard", () => { + it("includes update_multi: true so the card can be patched later", () => { + const card = buildMarkdownCard("hello") as { config: { update_multi: boolean } }; + expect(card.config.update_multi).toBe(true); + }); + + it("places the body inside a markdown element", () => { + const card = buildMarkdownCard("body text") as { + elements: Array<{ tag: string; content?: string }>; + }; + expect(card.elements[0]).toEqual({ tag: "markdown", content: "body text" }); + }); + + it("appends extraElements after the body", () => { + const extra = { tag: "divider" }; + const card = buildMarkdownCard("body", { extraElements: [extra] }) as { + elements: Array>; + }; + expect(card.elements).toHaveLength(2); + expect(card.elements[1]).toEqual(extra); + }); + + it("attaches an optional header with template color", () => { + const card = buildMarkdownCard("body", { + header: { title: "Hello", template: "blue" }, + }) as { header?: { title: { content: string }; template?: string } }; + expect(card.header?.title.content).toBe("Hello"); + expect(card.header?.template).toBe("blue"); + }); + }); + + describe("buildActionButtonsCard", () => { + it("creates one button per option with the correct value payload", () => { + const card = buildActionButtonsCard( + "Pick one", + [ + { text: "Yes", value: { id: "y" }, type: "primary" }, + { text: "No", value: { id: "n" } }, + ], + ) as { elements: Array> }; + + // Last element should be the action row with both buttons + const actionRow = card.elements[card.elements.length - 1] as { + tag: string; + actions: Array<{ value: Record; type: string; text: { content: string } }>; + }; + expect(actionRow.tag).toBe("action"); + expect(actionRow.actions).toHaveLength(2); + expect(actionRow.actions[0].value).toEqual({ id: "y" }); + expect(actionRow.actions[0].type).toBe("primary"); + expect(actionRow.actions[1].type).toBe("default"); + }); + }); + + describe("formatToolCall", () => { + const meta: ToolCallMeta = { + id: "t1", + name: "Read", + kind: "read", + status: "completed", + rawInput: { path: "src/foo.ts" }, + displaySummary: "Read src/foo.ts", + }; + + it("renders the tool icon and bolded label at medium verbosity", () => { + const out = formatToolCall(meta, "medium"); + expect(out).toContain("**Read src/foo.ts**"); + }); + + it("uses display title (not summary) at low verbosity", () => { + const titleMeta: ToolCallMeta = { ...meta, displayTitle: "src/foo.ts" }; + const out = formatToolCall(titleMeta, "low"); + expect(out).toContain("src/foo.ts"); + }); + + it("includes Input and Output sections at high verbosity", () => { + const highMeta: ToolCallMeta = { ...meta, content: "file contents here" }; + const out = formatToolCall(highMeta, "high"); + expect(out).toContain("**Input:**"); + expect(out).toContain("**Output:**"); + }); + + it("renders viewer file/diff links when provided regardless of verbosity", () => { + const linkedMeta: ToolCallMeta = { + ...meta, + viewerLinks: { file: "https://x/file", diff: "https://x/diff" }, + viewerFilePath: "src/foo.ts", + }; + const out = formatToolCall(linkedMeta, "medium"); + expect(out).toContain("View foo.ts"); + expect(out).toContain("View diff"); + }); + }); + + describe("formatToolUpdate", () => { + it("uses the same formatting as formatToolCall", () => { + const meta: ToolUpdateMeta = { + id: "u1", + name: "Edit", + status: "completed", + rawInput: {}, + }; + const callOut = formatToolCall(meta as ToolCallMeta, "medium"); + const updateOut = formatToolUpdate(meta, "medium"); + expect(updateOut).toBe(callOut); + }); + }); + + describe("formatPlan", () => { + it("uses status icons for each entry and 1-indexed numbering", () => { + const out = formatPlan({ + entries: [ + { content: "Task A", status: "completed" }, + { content: "Task B", status: "in_progress" }, + { content: "Task C", status: "pending" }, + ], + }); + expect(out).toContain("✅ 1. Task A"); + expect(out).toContain("🔄 2. Task B"); + expect(out).toContain("⬜ 3. Task C"); + }); + + it("escapes user content in plan entries", () => { + const out = formatPlan({ + entries: [{ content: "Build